From 4fda4061042fd74bd2a5ad30b92d02aa605e95a9 Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Mon, 28 Jul 2014 18:38:50 +0200 Subject: [PATCH] Implemented FacebookErrorHandler to handle facebook-error-codes --- pom.xml | 29 + .../java/de/juplo/facebook/FacebookUtils.java | 7 + .../juplo/facebook/GraphApiErrorHandler.java | 79 +++ .../de/juplo/facebook/GraphApiException.java | 64 ++ ...GraphApiExceptionJackson1Deserializer.java | 93 +++ .../GraphApiExceptionJackson1Serializer.java | 32 + ...GraphApiExceptionJackson2Deserializer.java | 100 +++ .../GraphApiExceptionJackson2Serializer.java | 37 + .../juplo/facebook/GraphMethodException.java | 14 + .../de/juplo/facebook/OAuthException.java | 14 + .../juplo/facebook/PageMigratedException.java | 41 ++ .../facebook/UnexpectedErrorException.java | 14 + .../juplo/facebook/UnknownErrorException.java | 14 + .../UnsupportedGetRequestException.java | 14 + .../facebook/GraphApiErrorHandlerTest.java | 637 ++++++++++++++++++ .../MockClientHttpRequestFactory.java | 144 ++++ src/test/resources/logback-test.xml | 19 + .../spring/test-facebook-error-handler.xml | 25 + 18 files changed, 1377 insertions(+) create mode 100644 src/main/java/de/juplo/facebook/GraphApiErrorHandler.java create mode 100644 src/main/java/de/juplo/facebook/GraphApiException.java create mode 100644 src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java create mode 100644 src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Serializer.java create mode 100644 src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java create mode 100644 src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Serializer.java create mode 100644 src/main/java/de/juplo/facebook/GraphMethodException.java create mode 100644 src/main/java/de/juplo/facebook/OAuthException.java create mode 100644 src/main/java/de/juplo/facebook/PageMigratedException.java create mode 100644 src/main/java/de/juplo/facebook/UnexpectedErrorException.java create mode 100644 src/main/java/de/juplo/facebook/UnknownErrorException.java create mode 100644 src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java create mode 100644 src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java create mode 100644 src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java create mode 100644 src/test/resources/logback-test.xml create mode 100644 src/test/resources/spring/test-facebook-error-handler.xml diff --git a/pom.xml b/pom.xml index 9a5597d..65f2569 100644 --- a/pom.xml +++ b/pom.xml @@ -42,9 +42,12 @@ 1.7 2.3.2 + 4.11 + 1.1.2 3.0.1 1.7.6 3.2.4.RELEASE + 3.2.4.RELEASE 3.1.3.RELEASE 1.0.5.RELEASE @@ -180,6 +183,32 @@ ${slf4j.version} + + + junit + junit + ${junit.version} + test + + + org.springframework + spring-test + ${springframework.version} + test + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + test + + + ch.qos.logback + logback-classic + ${logback.version} + test + + diff --git a/src/main/java/de/juplo/facebook/FacebookUtils.java b/src/main/java/de/juplo/facebook/FacebookUtils.java index bf0a5df..b55eb3b 100644 --- a/src/main/java/de/juplo/facebook/FacebookUtils.java +++ b/src/main/java/de/juplo/facebook/FacebookUtils.java @@ -16,6 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler; import org.springframework.security.oauth2.client.token.AccessTokenProvider; import org.springframework.security.oauth2.client.token.AccessTokenProviderChain; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider; @@ -112,6 +113,12 @@ public class FacebookUtils provider.setObjectMapper(objectMapper); chain.add(provider); template.setAccessTokenProvider(new AccessTokenProviderChain(chain)); + log.info("injecting GraphApiErrorHandler"); + template.setErrorHandler( + new GraphApiErrorHandler( + (OAuth2ErrorHandler)template.getErrorHandler() + ) + ); } return bean; diff --git a/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java b/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java new file mode 100644 index 0000000..ce4c98c --- /dev/null +++ b/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java @@ -0,0 +1,79 @@ +package de.juplo.facebook; + +import java.io.IOException; +import java.util.List; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler; +import org.springframework.web.client.HttpMessageConverterExtractor; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + + + +/** + * + * @author kai + */ +public class GraphApiErrorHandler extends OAuth2ErrorHandler +{ + private final OAuth2ErrorHandler errorHandler; + private List> messageConverters = + new RestTemplate().getMessageConverters(); + + + public GraphApiErrorHandler(OAuth2ErrorHandler errorHandler) + { + super(null); + this.errorHandler = errorHandler; + } + + + /** + * @param messageConverters the messageConverters to set + */ + @Override + public void setMessageConverters( + List> messageConverters + ) + { + this.messageConverters = messageConverters; + errorHandler.setMessageConverters(messageConverters); + } + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException + { + return errorHandler.hasError(response); + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException + { + HttpMessageConverterExtractor extractor = + new HttpMessageConverterExtractor<>( + GraphApiException.class, + messageConverters + ); + + try + { + GraphApiException body = extractor.extractData(response); + if (body != null) + { + // If we can get an OAuth2Exception already from the body, it is likely + // to have more information than the header does, so just re-throw it + // here. + body.setHttpErrorCode(response.getRawStatusCode()); + throw body; + } + } + catch (RestClientException|HttpMessageNotReadableException e) + { + // ignore + } + + errorHandler.handleError(response); + } +} diff --git a/src/main/java/de/juplo/facebook/GraphApiException.java b/src/main/java/de/juplo/facebook/GraphApiException.java new file mode 100644 index 0000000..47ea6c3 --- /dev/null +++ b/src/main/java/de/juplo/facebook/GraphApiException.java @@ -0,0 +1,64 @@ +package de.juplo.facebook; + +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + * Base exception for Facebook Graph-Api exceptions. + * + * @author Kai Moritz + */ +@org.codehaus.jackson.map.annotate.JsonSerialize(using = GraphApiExceptionJackson1Serializer.class) +@org.codehaus.jackson.map.annotate.JsonDeserialize(using = GraphApiExceptionJackson1Deserializer.class) +@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = GraphApiExceptionJackson2Serializer.class) +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = GraphApiExceptionJackson2Deserializer.class) +public class GraphApiException extends OAuth2Exception +{ + private final String type; + private final int code; + + private int httpErrorCode; + + + public GraphApiException(String message, String type, int code) + { + super(message); + this.type = type; + this.code = code; + } + + + public String getType() + { + return type; + } + + public int getCode() + { + return code; + } + + @Override + public int getHttpErrorCode() + { + return httpErrorCode == 0 ? super.getHttpErrorCode() : httpErrorCode; + } + + public void setHttpErrorCode(int httpErrorCode) + { + this.httpErrorCode = httpErrorCode; + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append("{error:{\"message\":\""); + builder.append(getMessage().replaceAll("\"", "\\\"")); + builder.append("\",\"type\":"); + builder.append(type.replaceAll("\"", "\\\"")); + builder.append("\",\"code\":"); + builder.append(code); + builder.append("}}"); + return builder.toString(); + } +} diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java new file mode 100644 index 0000000..2d0c002 --- /dev/null +++ b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java @@ -0,0 +1,93 @@ +package de.juplo.facebook; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.JsonToken; +import org.codehaus.jackson.map.DeserializationContext; +import org.codehaus.jackson.map.JsonDeserializer; + +/** + * @author Kai Moritz + */ +public class GraphApiExceptionJackson1Deserializer + extends + JsonDeserializer +{ + + @Override + public GraphApiException deserialize( + JsonParser jp, + DeserializationContext ctxt + ) + throws + IOException, + JsonProcessingException + { + JsonToken t = jp.getCurrentToken(); + if (t != JsonToken.START_OBJECT) + return null; + + t = jp.nextToken(); + if (t != JsonToken.FIELD_NAME) + return null; + + if (!jp.getCurrentName().equals("error")) + return null; + + t = jp.nextToken(); + if (t != JsonToken.START_OBJECT) + return null; + + String message = null, type = null; + Integer code = null; + + t = jp.nextToken(); + Map map = new HashMap<>(); + for (; t == JsonToken.FIELD_NAME; t = jp.nextToken()) + { + // Must point to field name + String fieldName = jp.getCurrentName(); + // And then the value... + t = jp.nextToken(); + + switch (t) + { + case VALUE_STRING: + switch(fieldName.toLowerCase()) + { + case "message": + message = jp.getText(); + break; + case "type": + type = jp.getText(); + break; + default: + return null; + } + break; + case VALUE_NUMBER_INT: + if (!fieldName.equalsIgnoreCase("code")) + return null; + code = jp.getValueAsInt(); + break; + default: + return null; + } + } + + if (message == null || type == null || code == null) + return null; + + switch (code) + { + case 1: return new UnknownErrorException(); + case 2: return new UnexpectedErrorException(); + case 21: return new PageMigratedException(message); + case 100: return new UnsupportedGetRequestException(); + default: return new GraphApiException(message, type, code); + } + } +} diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Serializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Serializer.java new file mode 100644 index 0000000..db7497f --- /dev/null +++ b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Serializer.java @@ -0,0 +1,32 @@ +package de.juplo.facebook; + +import java.io.IOException; +import java.util.Map.Entry; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.map.JsonSerializer; +import org.codehaus.jackson.map.SerializerProvider; + +/** + * @author Dave Syer + * + */ +public class GraphApiExceptionJackson1Serializer extends JsonSerializer { + + @Override + public void serialize(GraphApiException value, JsonGenerator jgen, SerializerProvider provider) throws IOException, + JsonProcessingException { + jgen.writeStartObject(); + jgen.writeStringField("error", value.getOAuth2ErrorCode()); + jgen.writeStringField("error_description", value.getMessage()); + if (value.getAdditionalInformation()!=null) { + for (Entry entry : value.getAdditionalInformation().entrySet()) { + String key = entry.getKey(); + String add = entry.getValue(); + jgen.writeStringField(key, add); + } + } + jgen.writeEndObject(); + } + +} diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java new file mode 100644 index 0000000..ec46de3 --- /dev/null +++ b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java @@ -0,0 +1,100 @@ +package de.juplo.facebook; + + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + + +/** + * @author Kai Moritz + * + */ +public class GraphApiExceptionJackson2Deserializer + extends + StdDeserializer +{ + public GraphApiExceptionJackson2Deserializer() + { + super(GraphApiException.class); + } + + @Override + public GraphApiException deserialize( + JsonParser jp, + DeserializationContext ctxt + ) + throws + IOException, + JsonProcessingException + { + JsonToken t = jp.getCurrentToken(); + if (t != JsonToken.START_OBJECT) + return null; + + t = jp.nextToken(); + if (t != JsonToken.FIELD_NAME) + return null; + + if (!jp.getCurrentName().equals("error")) + return null; + + t = jp.nextToken(); + if (t != JsonToken.START_OBJECT) + return null; + + String message = null, type = null; + Integer code = null; + + t = jp.nextToken(); + Map map = new HashMap<>(); + for (; t == JsonToken.FIELD_NAME; t = jp.nextToken()) + { + // Must point to field name + String fieldName = jp.getCurrentName(); + // And then the value... + t = jp.nextToken(); + + switch (t) + { + case VALUE_STRING: + switch(fieldName.toLowerCase()) + { + case "message": + message = jp.getText(); + break; + case "type": + type = jp.getText(); + break; + default: + return null; + } + break; + case VALUE_NUMBER_INT: + if (!fieldName.equalsIgnoreCase("code")) + return null; + code = jp.getValueAsInt(); + break; + default: + return null; + } + } + + if (message == null || type == null || code == null) + return null; + + switch (code) + { + case 1: return new UnknownErrorException(); + case 2: return new UnexpectedErrorException(); + case 21: return new PageMigratedException(message); + case 100: return new UnsupportedGetRequestException(); + default: return new GraphApiException(message, type, code); + } + } +} diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Serializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Serializer.java new file mode 100644 index 0000000..23b02d8 --- /dev/null +++ b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Serializer.java @@ -0,0 +1,37 @@ +package de.juplo.facebook; + +import java.io.IOException; +import java.util.Map.Entry; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +/** + * @author Brian Clozel + * + */ +public class GraphApiExceptionJackson2Serializer extends StdSerializer { + + public GraphApiExceptionJackson2Serializer() { + super(GraphApiException.class); + } + + @Override + public void serialize(GraphApiException value, JsonGenerator jgen, SerializerProvider provider) throws IOException, + JsonProcessingException { + jgen.writeStartObject(); + jgen.writeStringField("error", value.getOAuth2ErrorCode()); + jgen.writeStringField("error_description", value.getMessage()); + if (value.getAdditionalInformation()!=null) { + for (Entry entry : value.getAdditionalInformation().entrySet()) { + String key = entry.getKey(); + String add = entry.getValue(); + jgen.writeStringField(key, add); + } + } + jgen.writeEndObject(); + } + +} diff --git a/src/main/java/de/juplo/facebook/GraphMethodException.java b/src/main/java/de/juplo/facebook/GraphMethodException.java new file mode 100644 index 0000000..934243c --- /dev/null +++ b/src/main/java/de/juplo/facebook/GraphMethodException.java @@ -0,0 +1,14 @@ +package de.juplo.facebook; + + +/** + * + * @author kai + */ +public abstract class GraphMethodException extends GraphApiException +{ + public GraphMethodException(String message, int code) + { + super(message, "GraphMethodException", code); + } +} diff --git a/src/main/java/de/juplo/facebook/OAuthException.java b/src/main/java/de/juplo/facebook/OAuthException.java new file mode 100644 index 0000000..1e80fe3 --- /dev/null +++ b/src/main/java/de/juplo/facebook/OAuthException.java @@ -0,0 +1,14 @@ +package de.juplo.facebook; + + +/** + * + * @author kai + */ +public abstract class OAuthException extends GraphApiException +{ + public OAuthException(String message, int code) + { + super(message, "OAuthException", code); + } +} diff --git a/src/main/java/de/juplo/facebook/PageMigratedException.java b/src/main/java/de/juplo/facebook/PageMigratedException.java new file mode 100644 index 0000000..c3da159 --- /dev/null +++ b/src/main/java/de/juplo/facebook/PageMigratedException.java @@ -0,0 +1,41 @@ +package de.juplo.facebook; + + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +/** + * + * @author kai + */ +public class PageMigratedException extends OAuthException +{ + private final static Pattern pattern = + Pattern.compile("Page ID ([0-9]+) was migrated to page ID ([0-9]+)"); + + private final Long oldId, newId; + + + public PageMigratedException(String message) + { + super(message, 21); + Matcher matcher = pattern.matcher(message); + if (!matcher.find()) + throw new RuntimeException("Could not parse migration-error: " + message); + oldId = Long.parseLong(matcher.group(1)); + newId = Long.parseLong(matcher.group(2)); + } + + + public Long getOldId() + { + return oldId; + } + + public Long getNewId() + { + return newId; + } +} diff --git a/src/main/java/de/juplo/facebook/UnexpectedErrorException.java b/src/main/java/de/juplo/facebook/UnexpectedErrorException.java new file mode 100644 index 0000000..031195f --- /dev/null +++ b/src/main/java/de/juplo/facebook/UnexpectedErrorException.java @@ -0,0 +1,14 @@ +package de.juplo.facebook; + + +/** + * + * @author kai + */ +public class UnexpectedErrorException extends OAuthException +{ + public UnexpectedErrorException() + { + super("An unexpected error has occurred. Please retry your request later.", 2); + } +} diff --git a/src/main/java/de/juplo/facebook/UnknownErrorException.java b/src/main/java/de/juplo/facebook/UnknownErrorException.java new file mode 100644 index 0000000..9790973 --- /dev/null +++ b/src/main/java/de/juplo/facebook/UnknownErrorException.java @@ -0,0 +1,14 @@ +package de.juplo.facebook; + + +/** + * + * @author kai + */ +public class UnknownErrorException extends OAuthException +{ + public UnknownErrorException() + { + super("An unknown error has occurred.", 1); + } +} diff --git a/src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java b/src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java new file mode 100644 index 0000000..f1020c1 --- /dev/null +++ b/src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java @@ -0,0 +1,14 @@ +package de.juplo.facebook; + + +/** + * + * @author kai + */ +public class UnsupportedGetRequestException extends GraphMethodException +{ + public UnsupportedGetRequestException() + { + super("Unsupported get request.", 100); + } +} diff --git a/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java b/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java new file mode 100644 index 0000000..b9dc905 --- /dev/null +++ b/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java @@ -0,0 +1,637 @@ +package de.juplo.facebook; + +import java.util.Date; +import java.util.Map; +import java.util.Set; +import javax.annotation.Resource; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException; +import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; +import org.springframework.security.oauth2.client.token.AccessTokenProvider; +import org.springframework.security.oauth2.client.token.AccessTokenRequest; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import static org.springframework.security.oauth2.common.OAuth2AccessToken.OAUTH2_TYPE; +import org.springframework.security.oauth2.common.OAuth2RefreshToken; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + + + +/** + * + * @author kai + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration( + locations = { + "classpath:/spring/test-facebook-error-handler.xml" + }) +public class GraphApiErrorHandlerTest +{ + private static final Logger log = + LoggerFactory.getLogger(GraphApiErrorHandlerTest.class); + + @Resource + private OAuth2RestTemplate clientTemplate; + + private MockClientHttpRequestFactory requestFactory; + + + @Test + public void testError1() + { + log.info("testError1"); + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"An unknown error has occurred.\",\n" + + " \"type\": \"OAuthException\",\n" + + " \"code\": 1\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(UnknownErrorException e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertEquals(1, e.getCode()); + assertEquals("An unknown error has occurred.", e.getMessage()); + assertEquals("OAuthException", e.getType()); + } + } + + @Test + public void testError2() + { + log.info("testError2"); + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"An unexpected error has occurred. Please retry your request later.\",\n" + + " \"type\": \"OAuthException\",\n" + + " \"code\": 2\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(UnexpectedErrorException e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertEquals(2, e.getCode()); + assertEquals("An unexpected error has occurred. Please retry your request later.", e.getMessage()); + assertEquals("OAuthException", e.getType()); + } + } + + @Test + public void testError21() + { + log.info("testError21"); + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"(#21) Page ID 590408587650316 was migrated to page ID 1421620791415603. Please update your API calls to the new ID\",\n" + + " \"type\": \"OAuthException\",\n" + + " \"code\": 21\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(PageMigratedException e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertEquals(21, e.getCode()); + assertEquals("(#21) Page ID 590408587650316 was migrated to page ID 1421620791415603. Please update your API calls to the new ID", e.getMessage()); + assertEquals("OAuthException", e.getType()); + } + } + + @Test + public void testError100() + { + log.info("testError100"); + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"Unsupported get request.\",\n" + + " \"type\": \"GraphMethodException\",\n" + + " \"code\": 100\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(UnsupportedGetRequestException e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertEquals(100, e.getCode()); + assertEquals("Unsupported get request.", e.getMessage()); + assertEquals("GraphMethodException", e.getType()); + } + } + + @Test + public void testUnmappedError() + { + log.info("testUnmappedError"); + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"This error does not exist.\",\n" + + " \"type\": \"NonexistentException\",\n" + + " \"code\": 999999999\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(GraphApiException e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertEquals(999999999, e.getCode()); + assertEquals("This error does not exist.", e.getMessage()); + assertEquals("NonexistentException", e.getType()); + } + } + + @Test + public void testInvlalidErrors() + { + log.info("testInvalidErrors"); + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": null,\n" + + " \"type\": \"Whatever\",\n" + + " \"code\": 999999999\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"type\": \"Whatever\",\n" + + " \"code\": 999999999\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"Not a Graph-Api-Exception.\",\n" + + " \"type\": null,\n" + + " \"code\": 999999999\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"Not a Graph-Api-Exception.\",\n" + + " \"code\": 999999999\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"Not a Graph-Api-Exception.\",\n" + + " \"type\": \"Whatever\",\n" + + " \"code\": \"some string\"\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"Not a Graph-Api-Exception.\",\n" + + " \"type\": \"Whatever\",\n" + + " \"code\": 9.9\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"Not a Graph-Api-Exception.\",\n" + + " \"type\": \"Whatever\",\n" + + " \"code\": null\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody( + "{\n" + + " \"error\":\n" + + " {\n" + + " \"message\": \"Not a Graph-Api-Exception.\",\n" + + " \"type\": \"Whatever\"\n" + + " }\n" + + "}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{\"error\":{\"message\":null}}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{\"error\":{\"type\":null}}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{\"error\":{\"code\":null}}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{\"error\":{}}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{\"error\":\"some message\"}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{\"error\":null}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{\"some filed\":\"some message\"}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody("{}"); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(OAuth2Exception e) + { + log.debug("{}", e.toString()); + assertEquals("invalid_request", e.getOAuth2ErrorCode()); + assertFalse(e instanceof GraphApiException); + } + + + requestFactory.setBody(""); + + try + { + clientTemplate.getForObject("ANY", SOME.class); + fail("The expected exception was not thrown"); + } + catch(HttpMessageNotReadableException e) + { + // TODO: OAuth2ErrorHandler fails, if body contains no valid JSON! + log.debug("{}", e.toString()); + } + } + + + @Before + public void setUp() + { + requestFactory = new MockClientHttpRequestFactory(); + requestFactory.setStatus(HttpStatus.BAD_REQUEST); + requestFactory.addHeader("Content-Type", "application/json"); + clientTemplate.setRequestFactory(requestFactory); + + clientTemplate.setErrorHandler( + new GraphApiErrorHandler( + (OAuth2ErrorHandler)clientTemplate.getErrorHandler() + ) + ); + + clientTemplate.setAccessTokenProvider(new AccessTokenProvider() + { + @Override + public OAuth2AccessToken obtainAccessToken( + OAuth2ProtectedResourceDetails details, + AccessTokenRequest parameters + ) + throws + UserRedirectRequiredException, + UserApprovalRequiredException, + AccessDeniedException + { + return new OAuth2AccessToken() { + + @Override + public Map getAdditionalInformation() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Set getScope() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public OAuth2RefreshToken getRefreshToken() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getTokenType() + { + return OAUTH2_TYPE; + } + + @Override + public boolean isExpired() + { + return false; + } + + @Override + public Date getExpiration() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getExpiresIn() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getValue() + { + return "ANY"; + } + }; + } + + @Override + public boolean supportsResource(OAuth2ProtectedResourceDetails resource) + { + return true; + } + + @Override + public OAuth2AccessToken refreshAccessToken( + OAuth2ProtectedResourceDetails resource, + OAuth2RefreshToken refreshToken, + AccessTokenRequest request + ) + throws + UserRedirectRequiredException + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean supportsRefresh(OAuth2ProtectedResourceDetails resource) + { + return false; + } + }); + } + + + static class SOME + { + } +} diff --git a/src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java b/src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java new file mode 100644 index 0000000..3162182 --- /dev/null +++ b/src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java @@ -0,0 +1,144 @@ +package de.juplo.facebook; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; + + + +/** + * + * @author kai + */ +public class MockClientHttpRequestFactory implements ClientHttpRequestFactory +{ + private static final Logger log = + LoggerFactory.getLogger(MockClientHttpRequestFactory.class); + + private HttpStatus status = HttpStatus.OK; + private HttpHeaders headers = new HttpHeaders(); + private String body = ""; + + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod method) throws IOException + { + return new MockClientHttpRequest(uri, method); + } + + public void setStatus(HttpStatus status) + { + this.status = status; + } + + public void setHeaders(HttpHeaders headers) + { + this.headers = headers; + } + + public void addHeader(String name, String value) + { + headers.add(name, value); + } + + public void setBody(String body) + { + log.trace(body); + this.body = body; + } + + + class MockClientHttpRequest implements ClientHttpRequest + { + private final URI uri; + private final HttpMethod method; + + + public MockClientHttpRequest(URI uri, HttpMethod method) + { + this.uri = uri; + this.method = method; + } + + + @Override + public ClientHttpResponse execute() throws IOException + { + return new MockClientHttpResponse(); + } + + @Override + public HttpMethod getMethod() + { + return method; + } + + @Override + public URI getURI() + { + return uri; + } + + @Override + public HttpHeaders getHeaders() + { + return headers; + } + + @Override + public OutputStream getBody() throws IOException + { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + + class MockClientHttpResponse implements ClientHttpResponse + { + @Override + public HttpStatus getStatusCode() throws IOException + { + return status; + } + + @Override + public int getRawStatusCode() throws IOException + { + return status.value(); + } + + @Override + public String getStatusText() throws IOException + { + return status.getReasonPhrase(); + } + + @Override + public void close() + { + } + + @Override + public InputStream getBody() throws IOException + { + return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public HttpHeaders getHeaders() + { + return headers; + } + } +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..0638ecb --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + + + %p - %c{0}.%M\(%L\) | %m%n + + + + + + + + + + + + + diff --git a/src/test/resources/spring/test-facebook-error-handler.xml b/src/test/resources/spring/test-facebook-error-handler.xml new file mode 100644 index 0000000..e1b5629 --- /dev/null +++ b/src/test/resources/spring/test-facebook-error-handler.xml @@ -0,0 +1,25 @@ + + + + + + + + -- 2.20.1