package io.oasp.module.rest.service.impl; import io.oasp.module.test.common.base.ModuleTest; import java.util.List; import java.util.Map; import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.Validation; import javax.validation.ValidationException; import javax.validation.Validator; import javax.validation.constraints.Min; import javax.ws.rs.BadRequestException; import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import net.sf.mmm.util.exception.api.IllegalCaseException; import net.sf.mmm.util.exception.api.NlsRuntimeException; import net.sf.mmm.util.exception.api.ObjectNotFoundUserException; import net.sf.mmm.util.exception.api.TechnicalErrorUserException; import net.sf.mmm.util.lang.api.StringUtil; import net.sf.mmm.util.security.api.SecurityErrorUserException; import net.sf.mmm.util.validation.api.ValidationErrorUserException; import org.junit.Test; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AccountExpiredException; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import com.fasterxml.jackson.databind.ObjectMapper; /** * Test-case for {@link RestServiceExceptionFacade}. * */ public class RestServiceExceptionFacadeTest extends ModuleTest { /** Value of {@link TechnicalErrorUserException#getCode()}. */ private static final String CODE_TECHNICAL_ERROR = "TechnicalError"; /** Placeholder for any UUID. */ private static final String UUID_ANY = "<any-uuid>"; /** * @return the {@link RestServiceExceptionFacade} instance to test. */ protected RestServiceExceptionFacade getExceptionFacade() { RestServiceExceptionFacade facade = new RestServiceExceptionFacade(); facade.setMapper(new ObjectMapper()); return facade; } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with constraint violations */ @Test public void testConstraintViolationExceptions() { class CounterTest { @Min(value = 10) private Integer count; public CounterTest(Integer count) { this.count = count; } } CounterTest counter = new CounterTest(new Integer(1)); Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); Set<ConstraintViolation<CounterTest>> violations = validator.validate(counter); RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String message = "{count=[" + violations.iterator().next().getMessage() + "]}"; String errors = "{count=[" + violations.iterator().next().getMessage() + "]}"; Throwable error = new ConstraintViolationException(violations); checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE, errors); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with forbidden security exception including * subclasses. */ @Test public void testSecurityExceptions() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String secretMessage = "Secret information not to be revealed on client - only to be logged on server!"; int statusCode = 403; String message = "forbidden"; String code = null; checkFacade(exceptionFacade, new AccessDeniedException(secretMessage), statusCode, message, UUID_ANY, code); checkFacade(exceptionFacade, new AuthenticationCredentialsNotFoundException(secretMessage), statusCode, message, UUID_ANY, code, null); checkFacade(exceptionFacade, new BadCredentialsException(secretMessage), statusCode, message, UUID_ANY, code); checkFacade(exceptionFacade, new AccountExpiredException(secretMessage), statusCode, message, UUID_ANY, code); checkFacade(exceptionFacade, new InternalAuthenticationServiceException(secretMessage), statusCode, message, UUID_ANY, code, null); SecurityErrorUserException error = new SecurityErrorUserException(); checkFacade(exceptionFacade, error, statusCode, message, error.getUuid().toString(), code); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with forbidden security exception including * subclasses. */ @Test public void testSecurityExceptionExposed() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); exceptionFacade.setExposeInternalErrorDetails(true); String secretMessage = "Secret information not to be revealed on client - only to be logged on server!"; int statusCode = 403; String message = "The operation failed due to security restrictions. Please contact the support in case of a permission problem."; String code = null; checkFacade(exceptionFacade, new AccessDeniedException(secretMessage), statusCode, "SecurityErrorUserException: " + message + StringUtil.LINE_SEPARATOR + "AccessDeniedException: " + secretMessage, UUID_ANY, code); } /** * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given * {@link Throwable}. * * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test. * @param error is the {@link Throwable} to convert. * @param statusCode is the expected {@link Response#getStatus() status} code. * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result. * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be * {@code null}. * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be * {@code null}. * @return the JSON result for potential further asserts. */ protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode, String message, String uuid, String code) { return checkFacade(exceptionFacade, error, statusCode, message, uuid, code, null); } /** * Checks that the specified {@link RestServiceExceptionFacade} provides the expected results for the given * {@link Throwable}. * * @param exceptionFacade is the {@link RestServiceExceptionFacade} to test. * @param error is the {@link Throwable} to convert. * @param statusCode is the expected {@link Response#getStatus() status} code. * @param message is the expected {@link Throwable#getMessage() error message} from the JSON result. * @param uuid is the expected {@link NlsRuntimeException#getUuid() UUID} from the JSON result. May be * {@code null}. * @param code is the expected {@link NlsRuntimeException#getCode() error code} from the JSON result. May be * {@code null}. * @param errors is the expected validation errors in a format key-value * @return the JSON result for potential further asserts. */ @SuppressWarnings("unchecked") protected String checkFacade(RestServiceExceptionFacade exceptionFacade, Throwable error, int statusCode, String message, String uuid, String code, String errors) { Response response = exceptionFacade.toResponse(error); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(statusCode); Object entity = response.getEntity(); assertThat(entity).isInstanceOf(String.class); String result = (String) entity; try { Map<String, Object> valueMap = exceptionFacade.getMapper().readValue(result, Map.class); String msg = message; if (msg == null) { msg = error.getLocalizedMessage(); } assertThat(valueMap.get(RestServiceExceptionFacade.KEY_MESSAGE)).isEqualTo(msg); if ((statusCode == 403) && (!exceptionFacade.isExposeInternalErrorDetails())) { assertThat(result).doesNotContain(error.getMessage()); } assertThat(valueMap.get(RestServiceExceptionFacade.KEY_CODE)).isEqualTo(code); String actualUuid = (String) valueMap.get(RestServiceExceptionFacade.KEY_UUID); if (UUID_ANY.equals(uuid)) { if (actualUuid == null) { fail("UUID expected but not found in response: " + result); } } else { assertThat(actualUuid).isEqualTo(uuid); } Map<String, List<String>> errorsMap = (Map<String, List<String>>) valueMap.get(RestServiceExceptionFacade.KEY_ERRORS); if (errors == null) { if (errorsMap != null) { fail("Errors do not expected but found in response: " + result); } else { assertThat(errorsMap).isEqualTo(errors); } } else { if (errorsMap != null) { assertThat(errorsMap.toString()).isEqualTo(errors); } else { fail("Errors expected but not found in response: " + result); } } } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } return result; } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including * subclasses. */ @Test public void testJaxrsInternalServerException() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String internalMessage = "The HTTP request is invalid"; int statusCode = 500; InternalServerErrorException error = new InternalServerErrorException(internalMessage); String expectedMessage = new TechnicalErrorUserException(error).getLocalizedMessage(); checkFacade(exceptionFacade, error, statusCode, expectedMessage, UUID_ANY, CODE_TECHNICAL_ERROR); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception. */ @Test public void testJaxrsBadRequestException() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String message = "The HTTP request is invalid"; Throwable error = new BadRequestException(message); checkFacade(exceptionFacade, error, 400, message, UUID_ANY, "400"); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with a {@link ValidationException}. */ @Test public void testValidationException() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String message = "Validation failed!"; Throwable error = new ValidationException(message); checkFacade(exceptionFacade, error, 400, message, UUID_ANY, ValidationErrorUserException.CODE); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including * subclasses. */ @Test public void testJaxrsNotFoundException() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String internalMessage = "Either the service URL is wrong or the requested resource does not exist"; checkFacade(exceptionFacade, new NotFoundException(internalMessage), 404, internalMessage, UUID_ANY, "404"); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including * subclasses. */ @Test public void testTechnicalJavaRuntimeServerException() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String secretMessage = "Internal server error occurred"; IllegalArgumentException error = new IllegalArgumentException(secretMessage); String expectedMessage = new TechnicalErrorUserException(error).getLocalizedMessage(); checkFacade(exceptionFacade, error, 500, expectedMessage, UUID_ANY, CODE_TECHNICAL_ERROR); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including * subclasses. */ @Test public void testTechnicalCustomRuntimeServerException() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); String message = "Internal server error occurred"; IllegalCaseException error = new IllegalCaseException(message); String expectedMessage = new TechnicalErrorUserException(error).getLocalizedMessage(); checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including * subclasses. */ @Test public void testTechnicalCustomRuntimeServerExceptionExposed() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); exceptionFacade.setExposeInternalErrorDetails(true); String message = "Internal server error occurred"; IllegalCaseException error = new IllegalCaseException(message); String expectedMessage = "TechnicalErrorUserException: An unexpected error has occurred! We apologize any inconvenience. Please try again later." + StringUtil.LINE_SEPARATOR + error.getClass().getSimpleName() + ": " + error.getLocalizedMessage(); checkFacade(exceptionFacade, error, 500, expectedMessage, error.getUuid().toString(), CODE_TECHNICAL_ERROR); } /** * Tests {@link RestServiceExceptionFacade#toResponse(Throwable)} with bad request technical exception including * subclasses. */ @Test public void testBusinessException() { RestServiceExceptionFacade exceptionFacade = getExceptionFacade(); ObjectNotFoundUserException error = new ObjectNotFoundUserException(4711L); checkFacade(exceptionFacade, error, 400, null, error.getUuid().toString(), "NotFound"); } }