From e14670814b2f640069e7c6ce04d502d52b069675 Mon Sep 17 00:00:00 2001 From: bbilger Date: Thu, 22 Jun 2017 00:23:48 +0200 Subject: [PATCH] add support for openwhisk web actions; #20 --- .../io/GatewayBinaryReadInterceptor.java | 15 +- .../io/GatewayBinaryWriteInterceptor.java | 16 +- .../handler/GatewayRequestHandlerTest.java | 6 +- .../GatewayRequestObjectHandlerIntTest.java | 14 +- .../GatewayBinaryResponseFilterIntTest.java | 44 +-- .../AbstractFeignLambdaServiceClientTest.java | 18 +- build.gradle | 5 +- .../ConditionalBase64ReadInterceptor.java | 53 +++ .../ConditionalBase64WriteInterceptor.java | 52 +++ .../DefaultJRestlessContainerRequestTest.java | 5 +- .../core/filter/cors/CorsHeadersTest.java | 12 + .../ConditionalBase64ReadInterceptorTest.java | 140 +++++++ ...ConditionalBase64WriteInterceptorTest.java | 127 ++++++ .../jrestless/core/util/HeaderUtilsTest.java | 6 + .../jrestless-openwhisk-core/build.gradle | 2 + .../webaction/io/WebActionRequest.java | 60 +++ .../README.md | 2 + .../build.gradle | 20 + .../openwhisk/webaction/WebActionConfig.java | 40 ++ .../webaction/WebActionHttpConfig.java | 39 ++ .../handler/WebActionHttpRequestHandler.java | 85 +++++ .../handler/WebActionRequestHandler.java | 207 ++++++++++ .../webaction/io/BinaryMediaTypeDetector.java | 115 ++++++ .../webaction/io/DefaultWebActionRequest.java | 110 ++++++ .../io/WebActionBase64ReadInterceptor.java | 50 +++ .../io/WebActionBase64WriteInterceptor.java | 50 +++ .../webaction/WebActionConfigTest.java | 21 + .../webaction/WebActionHttpConfigTest.java | 24 ++ .../WebActionHttpRequestHandlerIntTest.java | 360 ++++++++++++++++++ .../WebActionHttpRequestHandlerTest.java | 98 +++++ .../handler/WebActionRequestHandlerTest.java | 287 ++++++++++++++ .../io/DefaultWebActionRequestTest.java | 82 ++++ .../WebActionBase64ReadInterceptorTest.java | 104 +++++ .../WebActionBase64WriteInterceptorTest.java | 104 +++++ .../io/WebActionHttpResponseBuilder.java | 95 +++++ .../webaction/io/WebActionRequestBuilder.java | 100 +++++ settings.gradle | 2 +- test/jrestless-test/build.gradle | 4 +- .../test/DynamicJerseyTestRunner.java | 63 +++ .../main/java/com/jrestless/test/IOUtils.java | 88 +++++ .../test/UtilityClassCodeCoverageBumper.java | 27 ++ .../jrestless/test/AccessibleRunnerTest.java | 5 + .../ConstructorPreconditionsTesterTest.java | 9 + .../test/DynamicJerseyTestRunnerTest.java | 123 ++++++ .../java/com/jrestless/test/IOUtilsTest.java | 76 ++++ .../test/InvokableArgumentsArgumentTest.java | 23 ++ .../UtilityClassCodeCoverageBumperTest.java | 39 ++ 47 files changed, 2930 insertions(+), 97 deletions(-) create mode 100644 core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptor.java create mode 100644 core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptor.java create mode 100644 core/jrestless-core-container/src/test/java/com/jrestless/core/filter/cors/CorsHeadersTest.java create mode 100644 core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptorTest.java create mode 100644 core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptorTest.java create mode 100644 openwhisk/core/jrestless-openwhisk-core/build.gradle create mode 100644 openwhisk/core/jrestless-openwhisk-core/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionRequest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/README.md create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/build.gradle create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionConfig.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionHttpConfig.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandler.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandler.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/BinaryMediaTypeDetector.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptor.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptor.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionConfigTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionHttpConfigTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerIntTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandlerTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequestTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptorTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptorTest.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionHttpResponseBuilder.java create mode 100644 openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionRequestBuilder.java create mode 100644 test/jrestless-test/src/main/java/com/jrestless/test/DynamicJerseyTestRunner.java create mode 100644 test/jrestless-test/src/main/java/com/jrestless/test/IOUtils.java create mode 100644 test/jrestless-test/src/main/java/com/jrestless/test/UtilityClassCodeCoverageBumper.java create mode 100644 test/jrestless-test/src/test/java/com/jrestless/test/DynamicJerseyTestRunnerTest.java create mode 100644 test/jrestless-test/src/test/java/com/jrestless/test/IOUtilsTest.java create mode 100644 test/jrestless-test/src/test/java/com/jrestless/test/InvokableArgumentsArgumentTest.java create mode 100644 test/jrestless-test/src/test/java/com/jrestless/test/UtilityClassCodeCoverageBumperTest.java diff --git a/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryReadInterceptor.java b/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryReadInterceptor.java index 90b0a2f..d925d9e 100644 --- a/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryReadInterceptor.java +++ b/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryReadInterceptor.java @@ -15,14 +15,12 @@ */ package com.jrestless.aws.gateway.io; -import java.io.IOException; -import java.util.Base64; - import javax.annotation.Priority; import javax.ws.rs.Priorities; -import javax.ws.rs.ext.ReaderInterceptor; import javax.ws.rs.ext.ReaderInterceptorContext; +import com.jrestless.core.interceptor.ConditionalBase64ReadInterceptor; + /** * Read interceptor that decodes the response from base64 if the property * {@link GatewayBinaryReadInterceptor#PROPERTY_BASE_64_ENCODED_REQUEST @@ -50,16 +48,13 @@ */ // make sure this gets invoked before any encoding ReaderInterceptor @Priority(Priorities.ENTITY_CODER - GatewayBinaryReadInterceptor.PRIORITY_OFFSET) -public class GatewayBinaryReadInterceptor implements ReaderInterceptor { +public class GatewayBinaryReadInterceptor extends ConditionalBase64ReadInterceptor { public static final String PROPERTY_BASE_64_ENCODED_REQUEST = "base64EncodedAwsApiGatewayRequest"; static final int PRIORITY_OFFSET = 100; @Override - public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException { - if (Boolean.TRUE.equals(context.getProperty(PROPERTY_BASE_64_ENCODED_REQUEST))) { - context.setInputStream(Base64.getDecoder().wrap(context.getInputStream())); - } - return context.proceed(); + protected boolean isBase64(ReaderInterceptorContext context) { + return Boolean.TRUE.equals(context.getProperty(PROPERTY_BASE_64_ENCODED_REQUEST)); } } diff --git a/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryWriteInterceptor.java b/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryWriteInterceptor.java index 28c97fd..e41c892 100644 --- a/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryWriteInterceptor.java +++ b/aws/gateway/jrestless-aws-gateway-handler/src/main/java/com/jrestless/aws/gateway/io/GatewayBinaryWriteInterceptor.java @@ -15,14 +15,12 @@ */ package com.jrestless.aws.gateway.io; -import java.io.IOException; -import java.util.Base64; - import javax.annotation.Priority; import javax.ws.rs.Priorities; -import javax.ws.rs.ext.WriterInterceptor; import javax.ws.rs.ext.WriterInterceptorContext; +import com.jrestless.core.interceptor.ConditionalBase64WriteInterceptor; + /** * Write interceptor that encodes the response in base64 if the first @@ -45,16 +43,12 @@ */ // make sure this gets invoked after any encoding WriteInterceptor @Priority(Priorities.ENTITY_CODER - GatewayBinaryWriteInterceptor.PRIORITY_OFFSET) -public class GatewayBinaryWriteInterceptor implements WriterInterceptor { +public class GatewayBinaryWriteInterceptor extends ConditionalBase64WriteInterceptor { static final int PRIORITY_OFFSET = 100; @Override - public void aroundWriteTo(WriterInterceptorContext context) throws IOException { - Object headerValue = context.getHeaders().getFirst(GatewayBinaryResponseFilter.HEADER_BINARY_RESPONSE); - if (Boolean.TRUE.equals(headerValue)) { - context.setOutputStream(Base64.getEncoder().wrap(context.getOutputStream())); - } - context.proceed(); + protected boolean isBase64(WriterInterceptorContext context) { + return Boolean.TRUE.equals(context.getHeaders().getFirst(GatewayBinaryResponseFilter.HEADER_BINARY_RESPONSE)); } } diff --git a/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestHandlerTest.java b/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestHandlerTest.java index 1fcf3da..a979287 100644 --- a/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestHandlerTest.java +++ b/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestHandlerTest.java @@ -119,12 +119,10 @@ public void delegateRequest_ValidRequestAndNoReferencesGiven_ShouldNotFailOnRequ requestScopedInitializer.initialize(serviceLocator); } - - - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings("unchecked") private RequestScopedInitializer getSetRequestScopedInitializer(Context context, GatewayRequest request) { GatewayRequestAndLambdaContext reqAndContext = new GatewayRequestAndLambdaContext(request, context); - ArgumentCaptor containerEnhancerCaptor = ArgumentCaptor.forClass(Consumer.class); + ArgumentCaptor> containerEnhancerCaptor = ArgumentCaptor.forClass(Consumer.class); gatewayHandler.delegateRequest(reqAndContext); verify(container).handleRequest(any(), any(), any(), containerEnhancerCaptor.capture()); diff --git a/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestObjectHandlerIntTest.java b/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestObjectHandlerIntTest.java index db3d1ce..8456a4b 100644 --- a/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestObjectHandlerIntTest.java +++ b/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/handler/GatewayRequestObjectHandlerIntTest.java @@ -93,6 +93,7 @@ import com.jrestless.aws.security.CognitoUserPoolAuthorizerPrincipal; import com.jrestless.aws.security.CustomAuthorizerPrincipal; import com.jrestless.core.container.dpi.InstanceBinder; +import com.jrestless.test.IOUtils; public class GatewayRequestObjectHandlerIntTest { @@ -247,7 +248,7 @@ public void testBinaryBase64EncodingWithContentEncoding() throws IOException { assertTrue(response.isIsBase64Encoded()); byte[] bytes = Base64.getDecoder().decode(response.getBody()); InputStream unzipStream = new GZIPInputStream(new ByteArrayInputStream(bytes)); - assertEquals("test", new String(toBytes(unzipStream))); + assertEquals("test", IOUtils.toString(unzipStream)); assertNotNull(response.getHeaders().get(HttpHeaders.CONTENT_ENCODING)); } @@ -265,17 +266,6 @@ public void testNonBinaryNonBase64EncodingWithContentEncoding() throws IOExcepti assertNull(response.getHeaders().get(HttpHeaders.CONTENT_ENCODING)); } - private byte[] toBytes(InputStream is) throws IOException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - byte[] data = new byte[1024]; - while ((nRead = is.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, nRead); - } - buffer.flush(); - return buffer.toByteArray(); - } - private void testBase64Encoding(String resoruce) { DefaultGatewayRequest request = new DefaultGatewayRequestBuilder() .httpMethod("GET") diff --git a/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/io/GatewayBinaryResponseFilterIntTest.java b/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/io/GatewayBinaryResponseFilterIntTest.java index 14fe516..207e5f7 100644 --- a/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/io/GatewayBinaryResponseFilterIntTest.java +++ b/aws/gateway/jrestless-aws-gateway-handler/src/test/java/com/jrestless/aws/gateway/io/GatewayBinaryResponseFilterIntTest.java @@ -4,8 +4,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; @@ -21,7 +19,10 @@ import org.glassfish.jersey.test.JerseyTest; import org.junit.Test; -public class GatewayBinaryResponseFilterIntTest { +import com.jrestless.test.DynamicJerseyTestRunner; +import com.jrestless.test.IOUtils; + +public class GatewayBinaryResponseFilterIntTest extends DynamicJerseyTestRunner { @Test public void testBinaryZippedWhenZippingRequested() throws Exception { @@ -35,7 +36,7 @@ public void testBinaryZippedWhenZippingRequested() throws Exception { assertNotNull(response.getHeaderString(HttpHeaders.CONTENT_ENCODING)); assertNotNull(response.getHeaderString(GatewayBinaryResponseFilter.HEADER_BINARY_RESPONSE)); InputStream unzipStream = new GZIPInputStream(response.readEntity(InputStream.class)); - assertEquals("binary", new String(toBytes(unzipStream))); + assertEquals("binary", IOUtils.toString(unzipStream)); }); } @@ -94,7 +95,7 @@ public void testNonBinaryZippedWhenZippingRequestedAndNotBinaryCompressionOnly() assertNotNull(response.getHeaderString(HttpHeaders.CONTENT_ENCODING)); assertNotNull(response.getHeaderString(GatewayBinaryResponseFilter.HEADER_BINARY_RESPONSE)); InputStream unzipStream = new GZIPInputStream(response.readEntity(InputStream.class)); - assertEquals("non-binary", new String(toBytes(unzipStream))); + assertEquals("non-binary", new String(IOUtils.toBytes(unzipStream))); }); } @@ -112,23 +113,6 @@ public void testNonBinaryNotZippedWhenNoZippingRequestedAndNotBinaryCompressionO }); } - private void runJerseyTest(JerseyTest jerseyTest, ThrowingConsumer test) throws Exception { - try { - try { - jerseyTest.setUp(); - } catch (Exception e) { - throw new RuntimeException(e); - } - test.accept(jerseyTest); - } finally { - try { - jerseyTest.tearDown(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - private JerseyTest createJerseyTest(Boolean binaryCompressionOnly) { return new JerseyTest() { @@ -148,17 +132,6 @@ protected Application configure() { }; } - private byte[] toBytes(InputStream is) throws IOException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - byte[] data = new byte[1024]; - while ((nRead = is.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, nRead); - } - buffer.flush(); - return buffer.toByteArray(); - } - @Path("/") public static class TestResource { @GET @@ -172,9 +145,4 @@ public Response getNonBinary() { return Response.ok("non-binary").build(); } } - - @FunctionalInterface - private static interface ThrowingConsumer { - void accept(T in) throws Exception; - } } diff --git a/aws/service/jrestless-aws-service-feign-client/src/test/java/com/jrestless/aws/service/client/AbstractFeignLambdaServiceClientTest.java b/aws/service/jrestless-aws-service-feign-client/src/test/java/com/jrestless/aws/service/client/AbstractFeignLambdaServiceClientTest.java index 9355586..2f244fa 100644 --- a/aws/service/jrestless-aws-service-feign-client/src/test/java/com/jrestless/aws/service/client/AbstractFeignLambdaServiceClientTest.java +++ b/aws/service/jrestless-aws-service-feign-client/src/test/java/com/jrestless/aws/service/client/AbstractFeignLambdaServiceClientTest.java @@ -1,13 +1,10 @@ package com.jrestless.aws.service.client; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.Collections; @@ -15,9 +12,9 @@ import org.junit.Before; import org.junit.Test; -import com.jrestless.aws.service.client.AbstractFeignLambdaServiceClient; import com.jrestless.aws.service.io.ServiceRequest; import com.jrestless.aws.service.io.ServiceResponse; +import com.jrestless.test.IOUtils; public class AbstractFeignLambdaServiceClientTest { @@ -105,7 +102,7 @@ public void execute_ResponseBodyGiven_ShouldRespondWithBody() throws IOException FeignLambdaClientImpl client = new FeignLambdaClientImpl(serviceResponse); when(serviceResponse.getBody()).thenReturn("some body"); feign.Response response = client.execute(feignRequest, null); - assertArrayEquals("some body".getBytes(), toBytes(response.body().asInputStream())); + assertEquals("some body", IOUtils.toString(response.body().asInputStream())); } @Test @@ -170,17 +167,6 @@ public void execute_OptionsGiven_ShouldPassOptionsToInternalExec() throws IOExce assertEquals(null, client.getFeignRequestOptions()); } - private byte[] toBytes(InputStream is) throws IOException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - byte[] data = new byte[1024]; - while ((nRead = is.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, nRead); - } - buffer.flush(); - return buffer.toByteArray(); - } - private static class FeignLambdaClientImpl extends AbstractFeignLambdaServiceClient { private ServiceResponse response; private ServiceRequest serviceRequest; diff --git a/build.gradle b/build.gradle index d9f430d..684efc1 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,8 @@ subprojects { junit: 'junit:junit:4.12', mockito_core: 'org.mockito:mockito-core:2.2.0', guava_testlib: 'com.google.guava:guava-testlib:18.0', - jsonassert: 'org.skyscreamer:jsonassert:1.3.0' + jsonassert: 'org.skyscreamer:jsonassert:1.3.0', + gson: 'com.google.code.gson:gson:2.8.1' ] checkstyle { @@ -178,6 +179,8 @@ dependencies { compile project(":aws:service:jrestless-aws-service-handler") compile project(":aws:service:jrestless-aws-service-feign-client") compile project(":aws:sns:jrestless-aws-sns-handler") + compile project(":openwhisk:core:jrestless-openwhisk-core") + compile project(":openwhisk:webaction:jrestless-openwhisk-webaction-handler") compile project(":test:jrestless-test") } diff --git a/core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptor.java b/core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptor.java new file mode 100644 index 0000000..8c709d0 --- /dev/null +++ b/core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.core.interceptor; + +import java.io.IOException; +import java.util.Base64; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.ReaderInterceptorContext; + +/** + * Wraps the {@link ReaderInterceptorContext context's} input stream with a + * base64 decoder (RFC4648; not URL-safe) if {@link #isBase64(ReaderInterceptorContext)} + * returns true. + * + * @author Bjoern Bilger + * + */ +public abstract class ConditionalBase64ReadInterceptor implements ReaderInterceptor { + + @Override + public final Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException { + if (isBase64(context)) { + context.setInputStream(Base64.getDecoder().wrap(context.getInputStream())); + } + return context.proceed(); + } + + /** + * Returns true if the {@link ReaderInterceptorContext context's} + * input stream should be wrapped by a base64 decoder. + * + * @param context + * the response context + * @return {@code true} in case the context's input stream must be wrapped + * by a base64 encoder; {@code false} otherwise + */ + protected abstract boolean isBase64(ReaderInterceptorContext context); +} diff --git a/core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptor.java b/core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptor.java new file mode 100644 index 0000000..9bf0d63 --- /dev/null +++ b/core/jrestless-core-container/src/main/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.core.interceptor; + +import java.io.IOException; +import java.util.Base64; + +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; + +/** + * Wraps the {@link WriterInterceptorContext context's} output stream with a + * base64 encoder (RFC4648; not URL-safe) if {@link #isBase64(WriterInterceptorContext)} + * returns true. + * + * @author Bjoern Bilger + * + */ +public abstract class ConditionalBase64WriteInterceptor implements WriterInterceptor { + + @Override + public final void aroundWriteTo(WriterInterceptorContext context) throws IOException { + if (isBase64(context)) { + context.setOutputStream(Base64.getEncoder().wrap(context.getOutputStream())); + } + context.proceed(); + } + + /** + * Returns true if the {@link WriterInterceptorContext context's} + * output stream should be wrapped by a base64 encoder. + * + * @param context + * the response context + * @return {@code true} in case the context's output stream must be wrapped + * by a base64 encoder; {@code false} otherwise + */ + protected abstract boolean isBase64(WriterInterceptorContext context); +} diff --git a/core/jrestless-core-container/src/test/java/com/jrestless/core/container/io/DefaultJRestlessContainerRequestTest.java b/core/jrestless-core-container/src/test/java/com/jrestless/core/container/io/DefaultJRestlessContainerRequestTest.java index cfe616d..cb51f8b 100644 --- a/core/jrestless-core-container/src/test/java/com/jrestless/core/container/io/DefaultJRestlessContainerRequestTest.java +++ b/core/jrestless-core-container/src/test/java/com/jrestless/core/container/io/DefaultJRestlessContainerRequestTest.java @@ -8,7 +8,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.net.URI; import java.util.ArrayList; @@ -20,9 +19,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.io.CharStreams; import com.jrestless.test.ConstructorPreconditionsTester; import com.jrestless.test.CopyConstructorEqualsTester; +import com.jrestless.test.IOUtils; public class DefaultJRestlessContainerRequestTest { @@ -32,7 +31,7 @@ public void testGetters() throws IOException { new ByteArrayInputStream("123".getBytes()), ImmutableMap.of("a", ImmutableList.of("a0", "a1"))); assertEquals(URI.create("/123"), request.getBaseUri()); assertEquals(URI.create("/456"), request.getRequestUri()); - assertEquals("123", CharStreams.toString(new InputStreamReader(request.getEntityStream()))); + assertEquals("123", IOUtils.toString(request.getEntityStream())); assertEquals(ImmutableMap.of("a", ImmutableList.of("a0", "a1")), request.getHeaders()); } diff --git a/core/jrestless-core-container/src/test/java/com/jrestless/core/filter/cors/CorsHeadersTest.java b/core/jrestless-core-container/src/test/java/com/jrestless/core/filter/cors/CorsHeadersTest.java new file mode 100644 index 0000000..041adb9 --- /dev/null +++ b/core/jrestless-core-container/src/test/java/com/jrestless/core/filter/cors/CorsHeadersTest.java @@ -0,0 +1,12 @@ +package com.jrestless.core.filter.cors; + +import org.junit.Test; + +import com.jrestless.test.UtilityClassCodeCoverageBumper; + +public class CorsHeadersTest { + @Test + public void bumpCodeCoverageByInvokingThePrivateConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(CorsHeaders.class); + } +} diff --git a/core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptorTest.java b/core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptorTest.java new file mode 100644 index 0000000..3ed536f --- /dev/null +++ b/core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64ReadInterceptorTest.java @@ -0,0 +1,140 @@ +package com.jrestless.core.interceptor; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; +import java.util.Random; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.ext.ReaderInterceptorContext; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import com.jrestless.test.IOUtils; + +public class ConditionalBase64ReadInterceptorTest { + + private final NeverBase64ReadInterceptor neverBase64ReadInterceptor = spy(new NeverBase64ReadInterceptor()); + private final AlwaysBase64ReadInterceptor alwaysBase64ReadInterceptor = spy(new AlwaysBase64ReadInterceptor()); + + @Test + public void testWrapsInputStreamNever() throws WebApplicationException, IOException { + ReaderInterceptorContext context = mock(ReaderInterceptorContext.class); + InputStream is = mock(InputStream.class); + when(context.getInputStream()).thenReturn(is); + + neverBase64ReadInterceptor.aroundReadFrom(context); + + verify(neverBase64ReadInterceptor).isBase64(context); + verify(context).proceed(); + verifyNoMoreInteractions(context); + } + + @Test + public void testWrapsInputStreamAlways() throws WebApplicationException, IOException { + ReaderInterceptorContext context = mock(ReaderInterceptorContext.class); + InputStream is = mock(InputStream.class); + when(context.getInputStream()).thenReturn(is); + + alwaysBase64ReadInterceptor.aroundReadFrom(context); + + verifyZeroInteractions(is); + + ArgumentCaptor updatedIsCapture = ArgumentCaptor.forClass(InputStream.class); + verify(context).setInputStream(updatedIsCapture.capture()); + verify(context).proceed(); + verify(context).getInputStream(); + verifyNoMoreInteractions(context); + + InputStream updatedIs = updatedIsCapture.getValue(); + + verify(alwaysBase64ReadInterceptor).isBase64(context); + + // just make sure we have some wrapper + assertNotSame(is, updatedIs); + updatedIs.close(); + verify(is).close(); + } + + @Test(expected = Base64DecodingFailedException.class) + public void testDoesNotWrapInputStreamWithBase64UrlEncoder() throws IOException { + // should be KUra8+qaMAL+Kpv0/5pR6zm8/d4= + final String base64Bytes = "KUra8-qaMAL-Kpv0_5pR6zm8_d4="; + testBase64DecodingFails(base64Bytes); + } + + @Test(expected = Base64DecodingFailedException.class) + public void testDoesNotWrapInputStreamWithBase64MimeEncoder() throws IOException { + final byte[] bytes = new byte[200]; + new Random().nextBytes(bytes); + testBase64DecodingFails(Base64.getMimeEncoder().encodeToString(bytes)); + } + + @Test + public void testBase64Decoding() throws IOException { + final byte[] actualBytes = "test".getBytes(); + testBase64Decoding(Base64.getEncoder().encodeToString(actualBytes), actualBytes); + } + + private void testBase64Decoding(String base64InputString, byte[] expectedReadBytes) throws IOException { + assertArrayEquals(expectedReadBytes, IOUtils.toBytes(fetchWrappedInputStream(base64InputString))); + } + + private void testBase64DecodingFails(String base64InputString) throws WebApplicationException, IOException { + InputStream is = fetchWrappedInputStream(base64InputString); + try { + IOUtils.toBytes(is); + } catch (Exception e) { + throw new Base64DecodingFailedException(e); + } + } + + private InputStream fetchWrappedInputStream(String base64InputString) throws WebApplicationException, IOException { + + ReaderInterceptorContext context = mock(ReaderInterceptorContext.class); + ByteArrayInputStream bais = new ByteArrayInputStream(base64InputString.getBytes()); + when(context.getInputStream()).thenReturn(bais); + + ArgumentCaptor updatesIsCapture = ArgumentCaptor.forClass(InputStream.class); + + alwaysBase64ReadInterceptor.aroundReadFrom(context); + + verify(context).setInputStream(updatesIsCapture.capture()); + return updatesIsCapture.getValue(); + } + + private static class NeverBase64ReadInterceptor extends ConditionalBase64ReadInterceptor { + @Override + protected boolean isBase64(ReaderInterceptorContext context) { + return false; + } + } + + private static class AlwaysBase64ReadInterceptor extends ConditionalBase64ReadInterceptor { + @Override + protected boolean isBase64(ReaderInterceptorContext context) { + return true; + } + } + + private static final class Base64DecodingFailedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + Base64DecodingFailedException(Exception e) { + super(e); + } + + } +} diff --git a/core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptorTest.java b/core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptorTest.java new file mode 100644 index 0000000..2af6c4f --- /dev/null +++ b/core/jrestless-core-container/src/test/java/com/jrestless/core/interceptor/ConditionalBase64WriteInterceptorTest.java @@ -0,0 +1,127 @@ +package com.jrestless.core.interceptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Base64; +import java.util.Random; + +import javax.ws.rs.ext.WriterInterceptorContext; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class ConditionalBase64WriteInterceptorTest { + + private final NeverBase64WriteInterceptor neverBase64WriteInterceptor = spy(new NeverBase64WriteInterceptor()); + private final AlwaysBase64WriteInterceptor alwaysBase64WriteInterceptor = spy(new AlwaysBase64WriteInterceptor()); + + @Test + public void testWrapsOutputStreamNever() throws IOException { + + WriterInterceptorContext context = mock(WriterInterceptorContext.class); + OutputStream os = mock(OutputStream.class); + when(context.getOutputStream()).thenReturn(os); + + neverBase64WriteInterceptor.aroundWriteTo(context); + + verify(neverBase64WriteInterceptor).isBase64(context); + + verify(context).proceed(); + verifyNoMoreInteractions(context); + } + + @Test + public void testWrapsOutputStreamAlways() throws IOException { + + WriterInterceptorContext context = mock(WriterInterceptorContext.class); + OutputStream os = mock(OutputStream.class); + when(context.getOutputStream()).thenReturn(os); + + ArgumentCaptor updatedOsCapture = ArgumentCaptor.forClass(OutputStream.class); + + alwaysBase64WriteInterceptor.aroundWriteTo(context); + + verify(alwaysBase64WriteInterceptor).isBase64(context); + + verifyZeroInteractions(os); + + verify(context).setOutputStream(updatedOsCapture.capture()); + verify(context).proceed(); + verify(context).getOutputStream(); + verifyNoMoreInteractions(context); + OutputStream updatedOs = updatedOsCapture.getValue(); + + // just make sure we have some wrapper + assertNotSame(os, updatedOs); + updatedOs.close(); + verify(os).close(); + } + + @Test + public void testDoesNotWrapOutputStreamWithBase64UrlEncoder() throws IOException { + // a URL encoder would give "KUra8-qaMAL-Kpv0_5pR6zm8_d4=" + final String base64Bytes = "KUra8+qaMAL+Kpv0/5pR6zm8/d4="; + testBase64Encoding(Base64.getDecoder().decode(base64Bytes), base64Bytes); + } + + @Test + public void testDoesNotWrapOutputStreamWithBase64MimeEncoder() throws IOException { + /* + * a mime encoder is usually limited in size and would add newlines when the line limit is hit + * => let's generate a large string or rather byte array + */ + final byte[] bytes = new byte[200]; + new Random().nextBytes(bytes); + final String base64Bytes = Base64.getEncoder().encodeToString(bytes); + testBase64Encoding(Base64.getDecoder().decode(base64Bytes), base64Bytes); + } + + @Test + public void testBase64Encoding() throws IOException { + final byte[] byteToEncode = "test".getBytes(); + testBase64Encoding(byteToEncode, new String(Base64.getEncoder().encode(byteToEncode))); + } + + private void testBase64Encoding(byte[] bytes, String expectedBase64) throws IOException { + + WriterInterceptorContext context = mock(WriterInterceptorContext.class); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + when(context.getOutputStream()).thenReturn(baos); + + ArgumentCaptor updatesOsCapture = ArgumentCaptor.forClass(OutputStream.class); + + alwaysBase64WriteInterceptor.aroundWriteTo(context); + + verify(context).setOutputStream(updatesOsCapture.capture()); + OutputStream updatedOs = updatesOsCapture.getValue(); + + updatedOs.write(bytes); + updatedOs.close(); + assertEquals(expectedBase64, baos.toString()); + } + + + private static class NeverBase64WriteInterceptor extends ConditionalBase64WriteInterceptor { + @Override + protected boolean isBase64(WriterInterceptorContext context) { + return false; + } + } + + private static class AlwaysBase64WriteInterceptor extends ConditionalBase64WriteInterceptor { + @Override + protected boolean isBase64(WriterInterceptorContext context) { + return true; + } + } +} diff --git a/core/jrestless-core-container/src/test/java/com/jrestless/core/util/HeaderUtilsTest.java b/core/jrestless-core-container/src/test/java/com/jrestless/core/util/HeaderUtilsTest.java index 6e98991..9e29338 100644 --- a/core/jrestless-core-container/src/test/java/com/jrestless/core/util/HeaderUtilsTest.java +++ b/core/jrestless-core-container/src/test/java/com/jrestless/core/util/HeaderUtilsTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import com.google.common.collect.ImmutableMap; +import com.jrestless.test.UtilityClassCodeCoverageBumper; import jersey.repackaged.com.google.common.collect.ImmutableList; @@ -131,4 +132,9 @@ public void exandHeaders_AnyGiven_ShouldReturnImmutableMap() { public void exandHeaders_AnyGiven_ShouldReturnImmutableList() { expandHeaders(ImmutableMap.of("k", "v0")).get("k").add("v1"); } + + @Test + public void bumpCodeCoverageByInvokingThePrivateConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(HeaderUtils.class); + } } diff --git a/openwhisk/core/jrestless-openwhisk-core/build.gradle b/openwhisk/core/jrestless-openwhisk-core/build.gradle new file mode 100644 index 0000000..d8aadd1 --- /dev/null +++ b/openwhisk/core/jrestless-openwhisk-core/build.gradle @@ -0,0 +1,2 @@ +group = 'com.jrestless.openwhisk' +version = "${globaleModuleVersion}" diff --git a/openwhisk/core/jrestless-openwhisk-core/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionRequest.java b/openwhisk/core/jrestless-openwhisk-core/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionRequest.java new file mode 100644 index 0000000..602ec48 --- /dev/null +++ b/openwhisk/core/jrestless-openwhisk-core/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionRequest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import java.util.Map; + +/** + * The deserialized request passed to the (raw) Web Action. + *

+ * It can be injected into resources via {@code @Context}. + * + * @author Bjoern Bilger + * + */ +public interface WebActionRequest { + + /** + * The HTTP method of the request. + */ + String getHttpMethod(); + + /** + * The request headers. + */ + Map getHeaders(); + + /** + * The unmatched path of the request (matching stops after consuming the action extension). + */ + String getPath(); + + /** + * The namespace identifying the OpenWhisk authenticated subject. + */ + String getUser(); + + /** + * The request body entity, as a base64 encoded string when content is + * binary, or plain string otherwise. + */ + String getBody(); + + /** + * The query parameters from the request as an unparsed string. + */ + String getQuery(); +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/README.md b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/README.md new file mode 100644 index 0000000..e124ce5 --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/README.md @@ -0,0 +1,2 @@ +# jrestless-openwhisk-webaction-handler + diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/build.gradle b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/build.gradle new file mode 100644 index 0000000..dda392c --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/build.gradle @@ -0,0 +1,20 @@ +group = 'com.jrestless.openwhisk' +version = "${globaleModuleVersion}" + +dependencies { + compile project(':openwhisk:core:jrestless-openwhisk-core') + compile project(':core:jrestless-core-container') + compile( + libraries.gson + ) + testCompile project(":test:jrestless-test") + testCompile ( + libraries.junit, + libraries.mockito_core, + libraries.slf4j_simple, + libraries.jersey_media_json_jackson, + libraries.jsonassert, + libraries.jersey_test_core, + libraries.jersey_test_grizzly + ) +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionConfig.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionConfig.java new file mode 100644 index 0000000..456a3e6 --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionConfig.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction; + +import org.glassfish.jersey.server.ResourceConfig; + +import com.jrestless.core.filter.ApplicationPathFilter; +import com.jrestless.openwhisk.webaction.io.WebActionBase64ReadInterceptor; + +/** + * This resource config registers required and recommended filters and + * interceptors for reading (raw) Web Action requests. + *

+ * The following filters and interceptors are registered: + *

    + *
  1. {@link WebActionBase64ReadInterceptor} + *
  2. {@link ApplicationPathFilter} + *
+ * @author Bjoern Bilger + * + */ +public class WebActionConfig extends ResourceConfig { + public WebActionConfig() { + register(WebActionBase64ReadInterceptor.class); + register(ApplicationPathFilter.class); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionHttpConfig.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionHttpConfig.java new file mode 100644 index 0000000..772b36c --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/WebActionHttpConfig.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction; + +import com.jrestless.openwhisk.webaction.io.WebActionBase64WriteInterceptor; + +/** + * This resource config registers required and recommended filters and + * interceptors for reading (raw) Web Action requests and writing (raw) Web + * Action responses intended for "http response types". + *

+ * The following filters and interceptors are registered: + *

    + *
  1. {@link com.jrestless.openwhisk.webaction.io.WebActionBase64ReadInterceptor} + *
  2. {@link WebActionBase64WriteInterceptor} + *
+ * + * @author Bjoern Bilger + * + */ +public class WebActionHttpConfig extends WebActionConfig { + public WebActionHttpConfig() { + super(); + register(WebActionBase64WriteInterceptor.class); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandler.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandler.java new file mode 100644 index 0000000..72f9649 --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.handler; + +import static java.util.Objects.requireNonNull; + +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.ws.rs.core.Response.StatusType; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + +/** + * OpenWhisk Web Action request handler suitable for "http response types". + *

+ * Notes: + *

    + *
  1. The request handler depends on + * {@link com.jrestless.openwhisk.webaction.io.WebActionBase64WriteInterceptor + * WebActionBase64WriteInterceptor} which must be registered on the + * {@link org.glassfish.jersey.server.ResourceConfig ResourceConfig} - for + * example using {@link com.jrestless.openwhisk.webaction.WebActionHttpConfig + * WebActionHttpConfig} + *
+ *

+ * JSON schema of the response: + * + *

+ * {
+ *   "type": "object",
+ *   "properties": {
+ *     "headers": {
+ *       "type": "object",
+ *       "additionalProperties": {
+ *         "type": "string"
+ *       }
+ *     },
+ *     "body": {
+ *       "type": "string"
+ *     },
+ *     "satusCode": {
+ *       "type": "integer"
+ *     }
+ *   }
+ * }
+ * 
+ * + * @author Bjoern Bilger + * + */ +public class WebActionHttpRequestHandler extends WebActionRequestHandler { + + private static final Gson GSON = new GsonBuilder().create(); + + @Override + protected JsonObject createJsonResponse(@Nullable String body, @Nonnull Map responseHeaders, + @Nonnull StatusType statusType) { + requireNonNull(responseHeaders); + requireNonNull(statusType); + JsonObject response = new JsonObject(); + if (body != null) { + response.addProperty("body", body); + } + response.addProperty("statusCode", statusType.getStatusCode()); + response.add("headers", GSON.toJsonTree(responseHeaders, Map.class)); + return response; + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandler.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandler.java new file mode 100644 index 0000000..6e17bfc --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandler.java @@ -0,0 +1,207 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.handler; + +import static java.util.Objects.requireNonNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.Response.StatusType; + +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.Binder; +import org.glassfish.jersey.internal.inject.ReferencingFactory; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.server.ContainerRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.jrestless.core.container.dpi.AbstractReferencingBinder; +import com.jrestless.core.container.handler.SimpleRequestHandler; +import com.jrestless.core.container.io.DefaultJRestlessContainerRequest; +import com.jrestless.core.container.io.JRestlessContainerRequest; +import com.jrestless.core.container.io.RequestAndBaseUri; +import com.jrestless.core.util.HeaderUtils; +import com.jrestless.openwhisk.webaction.io.DefaultWebActionRequest; +import com.jrestless.openwhisk.webaction.io.WebActionRequest; + +/** + * Base OpenWhisk Web Action request handler. + *

+ * Notes: + *

    + *
  1. The request handler is suitable for "RAW HTTP handling", only. + *
  2. The request handler depends on + * {@link com.jrestless.openwhisk.webaction.io.WebActionBase64ReadInterceptor + * WebActionBase64ReadInterceptor} which must be registered on the + * {@link org.glassfish.jersey.server.ResourceConfig ResourceConfig} - for + * example using {@link com.jrestless.openwhisk.webaction.WebActionConfig + * WebActionConfig} + *
  3. Subclasses need to implement how the response is written back + *
+ * + * @author Bjoern Bilger + * + */ +public abstract class WebActionRequestHandler extends SimpleRequestHandler { + + private static final Logger LOG = LoggerFactory.getLogger(WebActionRequestHandler.class); + private static final Gson GSON = new GsonBuilder().create(); + private static final Type WEB_ACTION_REQUEST_TYPE = (new TypeLiteral>() { }).getType(); + + protected abstract JsonObject createJsonResponse(String body, Map responseHeaders, + StatusType statusType); + + public JsonObject delegateJsonRequest(@Nonnull JsonObject request) { + try { + requireNonNull(request, "request may not be null"); + return delegateRequest(GSON.fromJson(request, DefaultWebActionRequest.class)); + } catch (JsonSyntaxException e) { + LOG.error("request failed with", e); + return createJsonResponse(null, Collections.emptyMap(), Status.INTERNAL_SERVER_ERROR); + } + } + + @Override + protected JRestlessContainerRequest createContainerRequest(WebActionRequest request) { + requireNonNull(request); + final String httpMethod = requireNonNull(request.getHttpMethod(), "httpMethod must be given").toUpperCase(); + final String body = request.getBody(); + final Map requestHeaders = request.getHeaders(); + final Map> containerRequestHeaders; + if (requestHeaders == null) { + containerRequestHeaders = Collections.emptyMap(); + } else { + containerRequestHeaders = HeaderUtils.expandHeaders(request.getHeaders()); + } + InputStream entityStream; + if (body != null) { + entityStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); + } else { + entityStream = new ByteArrayInputStream(new byte[0]); + } + final RequestAndBaseUri requestAndBaseUri = getRequestAndBaseUri(request); + return new DefaultJRestlessContainerRequest(requestAndBaseUri, httpMethod, entityStream, + containerRequestHeaders); + } + + protected RequestAndBaseUri getRequestAndBaseUri(WebActionRequest request) { + final String path = request.getPath(); + final URI requestUri; + /* + * prepend "/" + */ + if (path == null || path.isEmpty()) { + requestUri = URI.create("/"); + } else if (!path.startsWith("/")) { + requestUri = URI.create("/" + path); + } else { + requestUri = URI.create(path); + } + /* + * we have to use "/" as base URI since there is no proper way to get + * the base URI from the request + */ + return new RequestAndBaseUri(URI.create("/"), requestUri); + } + + @Override + protected SimpleResponseWriter createResponseWriter( + WebActionRequest request) { + return new ResponseWriter(); + } + + @Override + protected JsonObject onRequestFailure(Exception e, WebActionRequest request, + JRestlessContainerRequest containerRequest) { + LOG.error("request failed", e); + return createJsonResponse(null, Collections.emptyMap(), Status.INTERNAL_SERVER_ERROR); + } + + @Override + protected void extendActualJerseyContainerRequest(ContainerRequest actualContainerRequest, + JRestlessContainerRequest containerRequest, WebActionRequest request) { + actualContainerRequest.setRequestScopedInitializer(locator -> { + Ref webActionRequestRef = locator + .>getService(WEB_ACTION_REQUEST_TYPE); + if (webActionRequestRef != null) { + webActionRequestRef.set(request); + } else { + LOG.error("WebActionBinder has not been registered. WebActionRequest injection won't work."); + } + }); + } + + @Override + protected final Binder createBinder() { + return new WebActionBinder(); + } + + private class ResponseWriter implements SimpleResponseWriter { + private JsonObject response; + + @Override + public OutputStream getEntityOutputStream() { + return new ByteArrayOutputStream(); + } + + @Override + public void writeResponse(StatusType statusType, Map> headers, + OutputStream entityOutputStream) throws IOException { + Map flattenedHeaders = HeaderUtils.flattenHeaders(headers); + String body = ((ByteArrayOutputStream) entityOutputStream).toString(StandardCharsets.UTF_8.name()); + response = createJsonResponse(body, flattenedHeaders, statusType); + } + + @Override + public JsonObject getResponse() { + return response; + } + } + + private static class WebActionBinder extends AbstractReferencingBinder { + @Override + public void configure() { + bindReferencingFactory(WebActionRequest.class, ReferencingWebActionRequestFactory.class, + new TypeLiteral>() { }); + } + } + + private static class ReferencingWebActionRequestFactory extends ReferencingFactory { + @Inject + ReferencingWebActionRequestFactory(final Provider> referenceFactory) { + super(referenceFactory); + } + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/BinaryMediaTypeDetector.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/BinaryMediaTypeDetector.java new file mode 100644 index 0000000..80b172d --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/BinaryMediaTypeDetector.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.core.MediaType; + +/** + * Utility class to check if a media type is considered to be a "binary" media + * type by OpenWhisk, or not. + * + * @author Bjoern Bilger + * + */ +final class BinaryMediaTypeDetector { + + private BinaryMediaTypeDetector() { + } + + /** + * all "notBinary" content types - as defined in Spray which is used by OpenWhisk + * + * https://github.com/spray/spray/blob/master/spray-http/src/main/scala/spray/http/MediaType.scala#L282. + */ + private static final Set NON_BINARY_CONTENT_TYPES; + static { + Set nonBinaryContentTypes = new HashSet<>(); + nonBinaryContentTypes.add(MediaType.APPLICATION_ATOM_XML); + nonBinaryContentTypes.add("application/javascript"); + nonBinaryContentTypes.add("application/rss+xml"); + nonBinaryContentTypes.add("application/soap+xml"); + nonBinaryContentTypes.add("application/vnd.google-earth.kml+xml"); + nonBinaryContentTypes.add("application/x-vrml"); + nonBinaryContentTypes.add("application/x-www-form-urlencoded"); + nonBinaryContentTypes.add("application/xhtml+xml"); + nonBinaryContentTypes.add("application/xml-dtd"); + nonBinaryContentTypes.add("application/xml"); + nonBinaryContentTypes.add("image/svg+xml"); + nonBinaryContentTypes.add("message/http"); + nonBinaryContentTypes.add("message/delivery-status"); + nonBinaryContentTypes.add("message/rfc822"); + nonBinaryContentTypes.add("text/asp"); + nonBinaryContentTypes.add("text/cache-manifest"); + nonBinaryContentTypes.add("text/calendar"); + nonBinaryContentTypes.add("text/css"); + nonBinaryContentTypes.add("text/csv"); + nonBinaryContentTypes.add("text/html"); + nonBinaryContentTypes.add("text/mcf"); + nonBinaryContentTypes.add("text/plain"); + nonBinaryContentTypes.add("text/richtext"); + nonBinaryContentTypes.add("text/tab-separated-values"); + nonBinaryContentTypes.add("text/uri-list"); + nonBinaryContentTypes.add("text/vnd.wap.wml"); + nonBinaryContentTypes.add("text/vnd.wap.wmlscript"); + nonBinaryContentTypes.add("text/x-asm"); + nonBinaryContentTypes.add("text/x-c"); + nonBinaryContentTypes.add("text/x-component"); + nonBinaryContentTypes.add("text/x-h"); + nonBinaryContentTypes.add("text/x-java-source"); + nonBinaryContentTypes.add("text/x-pascal"); + nonBinaryContentTypes.add("text/x-script"); + nonBinaryContentTypes.add("text/x-scriptcsh"); + nonBinaryContentTypes.add("text/x-scriptelisp"); + nonBinaryContentTypes.add("text/x-scriptksh"); + nonBinaryContentTypes.add("text/x-scriptlisp"); + nonBinaryContentTypes.add("text/x-scriptperl"); + nonBinaryContentTypes.add("text/x-scriptperl-module"); + nonBinaryContentTypes.add("text/x-scriptphyton"); + nonBinaryContentTypes.add("text/x-scriptrexx"); + nonBinaryContentTypes.add("text/x-scriptscheme"); + nonBinaryContentTypes.add("text/x-scriptsh"); + nonBinaryContentTypes.add("text/x-scripttcl"); + nonBinaryContentTypes.add("text/x-scripttcsh"); + nonBinaryContentTypes.add("text/x-scriptzsh"); + nonBinaryContentTypes.add("text/x-server-parsed-html"); + nonBinaryContentTypes.add("text/x-setext"); + nonBinaryContentTypes.add("text/x-sgml"); + nonBinaryContentTypes.add("text/x-speech"); + nonBinaryContentTypes.add("text/x-uuencode"); + nonBinaryContentTypes.add("text/x-vcalendar"); + nonBinaryContentTypes.add("text/x-vcard"); + nonBinaryContentTypes.add("text/xml"); + NON_BINARY_CONTENT_TYPES = Collections.unmodifiableSet(nonBinaryContentTypes); + } + + /** + * checks if a media type is considered to be a "binary" media type by + * OpenWhisk, or not. + * + * @param mediaType + * @return {@code true} if it's a binary media type, {@code false} else + */ + static boolean isBinaryMediaType(MediaType mediaType) { + if (mediaType == null) { + return true; + } + return !NON_BINARY_CONTENT_TYPES.contains(mediaType.toString()); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequest.java new file mode 100644 index 0000000..40f86df --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import java.util.Map; +import java.util.Objects; + +import com.google.gson.annotations.SerializedName; + +/** + * Implementation of {@link WebActionRequest}. + *

+ * The implementation makes sure that the request object can get de-serialized + * into this representation (using GSON). + * + * @author Bjoern Bilger + * + */ +public final class DefaultWebActionRequest implements WebActionRequest { + @SerializedName("__ow_method") + private String httpMethod; + @SerializedName("__ow_headers") + private Map headers; + @SerializedName("__ow_path") + private String path; + @SerializedName("__ow_user") + private String user; + @SerializedName("__ow_body") + private String body; + @SerializedName("__ow_query") + private String query; + + public DefaultWebActionRequest() { + } + // for testing, only + DefaultWebActionRequest(String httpMethod, Map headers, String path, String user, String body, + String query) { + this.httpMethod = httpMethod; + this.headers = headers; + this.path = path; + this.user = user; + this.body = body; + this.query = query; + } + + @Override + public String getHttpMethod() { + return httpMethod; + } + + @Override + public Map getHeaders() { + return headers; + } + + @Override + public String getPath() { + return path; + } + + @Override + public String getUser() { + return user; + } + + @Override + public String getBody() { + return body; + } + + @Override + public String getQuery() { + return query; + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!getClass().equals(other.getClass())) { + return false; + } + DefaultWebActionRequest castOther = (DefaultWebActionRequest) other; + return Objects.equals(httpMethod, castOther.httpMethod) && Objects.equals(headers, castOther.headers) + && Objects.equals(path, castOther.path) && Objects.equals(user, castOther.user) + && Objects.equals(body, castOther.body) && Objects.equals(query, castOther.query); + } + + @Override + public int hashCode() { + return Objects.hash(httpMethod, headers, path, user, body, query); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptor.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptor.java new file mode 100644 index 0000000..0669934 --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.ext.ReaderInterceptorContext; + +import com.jrestless.core.interceptor.ConditionalBase64ReadInterceptor; + +/** + * Read interceptor that decodes a potentially base64 encoded request body. + *

+ * Whether a body of a (raw) Web Action request is base64 encoded or not, is + * determined by {@link ReaderInterceptorContext#getMediaType()}. According to + * the OpenWhisk specification, the Spray framework is used to determine whether + * a body needs to be encoded with base64 or not when passed to the handler. + * see: + * https://github.com/spray/spray/blob/master/spray-http/src/main/scala/spray/http/MediaType.scala#L282 + * Thus we wrap {@link ReaderInterceptorContext#getInputStream()} with a base64 + * decoder if it's a binary media type. + * + * @author Bjoern Bilger + * + */ +//make sure this gets invoked before any encoding ReaderInterceptor +@Priority(Priorities.ENTITY_CODER - WebActionBase64ReadInterceptor.PRIORITY_OFFSET) +public class WebActionBase64ReadInterceptor extends ConditionalBase64ReadInterceptor { + + static final int PRIORITY_OFFSET = 100; + + @Override + protected boolean isBase64(ReaderInterceptorContext context) { + return BinaryMediaTypeDetector.isBinaryMediaType(context.getMediaType()); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptor.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptor.java new file mode 100644 index 0000000..d08c1dd --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/main/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.ext.WriterInterceptorContext; + +import com.jrestless.core.interceptor.ConditionalBase64WriteInterceptor; + +/** + * Write interceptor that encodes "binary" response bodies. + *

+ * Whether a body of a (raw) Web Action http response needs to be base64 encoded + * or not, is determined by {@link WriterInterceptorContext#getMediaType()}. + * According to the OpenWhisk specification, the Spray framework is used to + * determine whether a body needs to be encoded with base64 or not when passed + * to the handler. see: + * https://github.com/spray/spray/blob/master/spray-http/src/main/scala/spray/http/MediaType.scala#L282 + * Thus we wrap {@link WriterInterceptorContext#getOutputStream()} with a base64 + * encoder if it's a binary media type. + * + * @author Bjoern Bilger + * + */ +//make sure this gets invoked after any encoding WriteInterceptor +@Priority(Priorities.ENTITY_CODER - WebActionBase64WriteInterceptor.PRIORITY_OFFSET) +public class WebActionBase64WriteInterceptor extends ConditionalBase64WriteInterceptor { + + static final int PRIORITY_OFFSET = 100; + + @Override + protected boolean isBase64(WriterInterceptorContext context) { + return BinaryMediaTypeDetector.isBinaryMediaType(context.getMediaType()); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionConfigTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionConfigTest.java new file mode 100644 index 0000000..bf9e3b1 --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionConfigTest.java @@ -0,0 +1,21 @@ +package com.jrestless.openwhisk.webaction; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; + +import org.junit.Test; + +import com.google.common.collect.ImmutableSet; +import com.jrestless.core.filter.ApplicationPathFilter; +import com.jrestless.openwhisk.webaction.io.WebActionBase64ReadInterceptor; + +public class WebActionConfigTest { + @Test + public void testRegistersReadInterceptor() { + WebActionConfig config = new WebActionConfig(); + assertEquals(ImmutableSet.of(WebActionBase64ReadInterceptor.class, ApplicationPathFilter.class), + config.getClasses()); + assertEquals(Collections.emptySet(), config.getInstances()); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionHttpConfigTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionHttpConfigTest.java new file mode 100644 index 0000000..26aa98f --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/WebActionHttpConfigTest.java @@ -0,0 +1,24 @@ +package com.jrestless.openwhisk.webaction; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; + +import org.junit.Test; + +import com.google.common.collect.ImmutableSet; +import com.jrestless.core.filter.ApplicationPathFilter; +import com.jrestless.openwhisk.webaction.io.WebActionBase64ReadInterceptor; +import com.jrestless.openwhisk.webaction.io.WebActionBase64WriteInterceptor; + + +public class WebActionHttpConfigTest { + + @Test + public void testRegistersReadAndWriteInterceptor() { + WebActionHttpConfig config = new WebActionHttpConfig(); + assertEquals(ImmutableSet.of(WebActionBase64ReadInterceptor.class, WebActionBase64WriteInterceptor.class, + ApplicationPathFilter.class), config.getClasses()); + assertEquals(Collections.emptySet(), config.getInstances()); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerIntTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerIntTest.java new file mode 100644 index 0000000..2df8a0e --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerIntTest.java @@ -0,0 +1,360 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.handler; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Objects; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.JsonObject; +import com.jrestless.core.container.dpi.InstanceBinder; +import com.jrestless.openwhisk.webaction.WebActionHttpConfig; +import com.jrestless.openwhisk.webaction.io.WebActionBase64ReadInterceptor; +import com.jrestless.openwhisk.webaction.io.WebActionHttpResponseBuilder; +import com.jrestless.openwhisk.webaction.io.WebActionRequest; +import com.jrestless.openwhisk.webaction.io.WebActionRequestBuilder; + +public class WebActionHttpRequestHandlerIntTest { + + private WebActionHttpRequestHandler handler; + private TestService testService; + + @Before + public void setup() { + testService = mock(TestService.class); + handler = new WebActionHttpRequestHandler(); + ResourceConfig config = new WebActionHttpConfig(); + config.register(WebActionBase64ReadInterceptor.class); + config.register(new InstanceBinder.Builder().addInstance(testService, TestService.class).build()); + config.register(TestResource.class); + handler.init(config); + handler.start(); + } + + @Test + public void testGetText() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.GET) + .setPath("get-text") + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBody("test") // NOT base64 encoded! + .setContentType(MediaType.TEXT_PLAIN_TYPE) + .build(); + assertEquals(expectedResponse, actualResponse); + } + + @Test + public void testGetJson() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.GET) + .setPath("get-json") + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBodyBase64Encoded("{\"value\":\"test\"}") // base64 encoded + .setContentType(MediaType.APPLICATION_JSON_TYPE) + .build(); + assertEquals(expectedResponse, actualResponse); + } + + @Test + public void testGetBinary() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.GET) + .setPath("get-binary") + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBodyBase64Encoded("binary") // base64 encoded + .setContentType("image/png") + .build(); + assertEquals(expectedResponse, actualResponse); + } + + @Test + public void testPostStringAndGetString() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.POST) + .setPath("post-string-get-string") + .setBodyBase64Encoded("someBody") + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBody("someBody") + .setContentType(MediaType.TEXT_PLAIN_TYPE) + .build(); + assertEquals(expectedResponse, actualResponse); + verify(testService).injectedStringArg("someBody"); + } + + @Test + public void testPostStringAndGetJson() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.POST) + .setPath("post-string-get-json") + .setBodyBase64Encoded("someBody") + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBodyBase64Encoded("{\"value\":\"someBody\"}") + .setContentType(MediaType.APPLICATION_JSON_TYPE) + .build(); + assertEquals(expectedResponse, actualResponse); + verify(testService).injectedStringArg("someBody"); + } + + @Test + public void testPostJsonAndGetJson() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.POST) + .setPath("post-json-get-json") + .setBodyBase64Encoded("{\"value\":\"123\"}") + .setContentType(MediaType.APPLICATION_JSON) + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBodyBase64Encoded("{\"value\":\"123\"}") + .setContentType(MediaType.APPLICATION_JSON) + .build(); + assertEquals(expectedResponse, actualResponse); + verify(testService).injectedEntity(new Entity("123")); + } + + @Test + public void testPostJsonAndGetString() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.POST) + .setPath("post-json-get-string") + .setBodyBase64Encoded("{\"value\":\"123\"}") + .setContentType(MediaType.APPLICATION_JSON) + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBody("123") + .setContentType(MediaType.TEXT_PLAIN_TYPE) + .build(); + assertEquals(expectedResponse, actualResponse); + verify(testService).injectedEntity(new Entity("123")); + } + + @Test + public void testPostBinary() { + JsonObject request = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.POST) + .setPath("post-binary") + .setBodyBase64Encoded("binary") + .setContentType("image/png") + .buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + JsonObject expectedResponse = new WebActionHttpResponseBuilder() + .setBody("binary") + .setContentType(MediaType.TEXT_PLAIN_TYPE) + .build(); + assertEquals(expectedResponse, actualResponse); + verify(testService).injectedStringArg("binary"); + } + + @Test + public void testWebActionRequestInjection() { + WebActionRequestBuilder requestBuilder = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.GET) + .setPath("inject-webaction-request"); + JsonObject request = requestBuilder.buildJson(); + JsonObject actualResponse = handler.delegateJsonRequest(request); + assertEquals(WebActionHttpResponseBuilder.noContent(), actualResponse); + verify(testService).injectedWebActionRequest(requestBuilder.build()); + } + + @Test + public void testWebActionRequestMemberInjection() { + WebActionRequestBuilder requestBuilder0 = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.GET) + .setPath("inject-webaction-request-member0"); + WebActionRequestBuilder requestBuilder1 = new WebActionRequestBuilder() + .setHttpMethod(HttpMethod.GET) + .setPath("inject-webaction-request-member1"); + + JsonObject request0 = requestBuilder0.buildJson(); + JsonObject request1 = requestBuilder1.buildJson(); + JsonObject actualResponse0 = handler.delegateJsonRequest(request0); + JsonObject actualResponse1 = handler.delegateJsonRequest(request1); + assertEquals(WebActionHttpResponseBuilder.noContent(), actualResponse0); + assertEquals(WebActionHttpResponseBuilder.noContent(), actualResponse1); + + InOrder inOrder = Mockito.inOrder(testService); + // we cannot use the object since the proxy is "Not inside a request scope", anymore + inOrder.verify(testService).injectedStringArg(requestBuilder0.build().getPath()); + inOrder.verify(testService).injectedStringArg(requestBuilder1.build().getPath()); + } + + @Path("/") + @Singleton // singleton in order to test proxies + public static class TestResource { + + @Context + private WebActionRequest webActionRequestMember; + + private final TestService testService; + + @Inject + public TestResource(TestService testService) { + this.testService = testService; + } + + @GET + @Path("/get-text") + @Produces(MediaType.TEXT_PLAIN) + public Response getText() { + return Response.ok("test").build(); + } + + @GET + @Path("/get-json") + @Produces(MediaType.APPLICATION_JSON) + public Response getJson() { + return Response.ok(new Entity("test")).build(); + } + + @GET + @Path("/get-binary") + @Produces("image/png") + public byte[] getBinary() { + return "binary".getBytes(); + } + + @POST + @Path("/post-string-get-string") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public Response postStringGetString(String body) { + testService.injectedStringArg(body); + return Response.ok(body).build(); + } + + @POST + @Path("/post-string-get-json") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response postStringGetJson(String body) { + testService.injectedStringArg(body); + return Response.ok(new Entity(body)).build(); + } + + @POST + @Path("/post-json-get-json") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response postJsonGetJson(Entity entity) { + testService.injectedEntity(entity); + return Response.ok(entity).build(); + } + + @POST + @Path("/post-json-get-string") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + public Response postJsonGetString(Entity entity) { + testService.injectedEntity(entity); + return Response.ok(entity.getValue()).build(); + } + + @POST + @Path("/post-binary") + @Consumes("image/png") + @Produces(MediaType.TEXT_PLAIN) + public Response postJsonGetString(byte[] binary) { + testService.injectedStringArg(new String(binary)); + return Response.ok(new String(binary)).build(); + } + + @GET + @Path("/inject-webaction-request") + public void injectWebActionRequest(@Context WebActionRequest request) { + testService.injectedWebActionRequest(request); + } + + @Path("/inject-webaction-request-member0") + @GET + public void injectWebActionRequestAsMember0() { + testService.injectedStringArg(webActionRequestMember.getPath()); + } + + @Path("/inject-webaction-request-member1") + @GET + public void injectWebActionRequestAsMember1() { + testService.injectedStringArg(webActionRequestMember.getPath()); + } + } + + public static interface TestService { + void injectedWebActionRequest(WebActionRequest webActionRequest); + void injectedStringArg(String arg); + void injectedEntity(Entity entity); + } + + private static class Entity { + private final String value; + @JsonCreator + public Entity(@JsonProperty("value") String value) { + this.value = value; + } + public String getValue() { + return value; + } + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (!getClass().equals(other.getClass())) { + return false; + } + Entity castOther = (Entity) other; + return Objects.equals(value, castOther.value); + } + @Override + public int hashCode() { + return Objects.hash(value); + } + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerTest.java new file mode 100644 index 0000000..428cbd7 --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionHttpRequestHandlerTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.handler; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.Map; + +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.Response.StatusType; + +import org.junit.Test; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + +import jersey.repackaged.com.google.common.collect.ImmutableMap; + +public class WebActionHttpRequestHandlerTest { + + private static final Gson GSON = new GsonBuilder().create(); + + private WebActionHttpRequestHandler handler = new WebActionHttpRequestHandler(); + + @Test(expected = NullPointerException.class) + public void createJsonResponse_NullHeadersGiven_ShouldFailNpe() { + handler.createJsonResponse(null, null, Status.OK); + } + + @Test(expected = NullPointerException.class) + public void createJsonResponse_NullStatusTypeGiven_ShouldFailNpe() { + handler.createJsonResponse(null, Collections.emptyMap(), null); + } + + @Test + public void createJsonResponse_NoBodyGiven_ShouldCreateResponseWithoutBody() { + final Map headers = Collections.singletonMap("headerName", "headerValue"); + testResponse(null, headers, Status.NOT_FOUND); + } + + @Test + public void createJsonResponse_BodyGiven_ShouldCreateResponseWithBody() { + final Map headers = Collections.singletonMap("headerName", "headerValue"); + testResponse("someBody", headers, Status.NOT_FOUND); + } + + @Test + public void createJsonResponse_EmptyHeadersGiven_ShouldCreateResponseWithEmptyHeader() { + testResponse("someBody", Collections.emptyMap(), Status.NOT_FOUND); + } + + @Test + public void createJsonResponse_SingleHeaderGiven_ShouldCreateResponseWithSingleHeader() { + final Map headers = Collections.singletonMap("header", "value"); + testResponse(null, headers, Status.OK); + } + + @Test + public void createJsonResponse_MultipleHeadersGiven_ShouldCreateResponseWithAllHeaders() { + final Map headers = ImmutableMap.of("header1", "value1", "header2", "value2"); + testResponse(null, headers, Status.OK); + } + + private void testResponse(String body, Map headers, StatusType status) { + JsonObject actualResponse = handler.createJsonResponse(body, headers, status); + JsonObject expectedResponse = createResponse(body, headers, status); + assertEquals(expectedResponse, actualResponse); + } + + private static JsonObject createResponse(String body, Map headers, StatusType status) { + return createResponse(body, headers, status.getStatusCode()); + } + + private static JsonObject createResponse(String body, Map headers, int statusCode) { + JsonObject response = new JsonObject(); + if (body != null) { + response.addProperty("body", body); + } + response.addProperty("statusCode", statusCode); + response.add("headers", GSON.toJsonTree(headers, Map.class)); + return response; + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandlerTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandlerTest.java new file mode 100644 index 0000000..6b40d8a --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/handler/WebActionRequestHandlerTest.java @@ -0,0 +1,287 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.handler; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Type; +import java.net.URI; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; + +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.Response.StatusType; + +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.spi.RequestScopedInitializer; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.jrestless.core.container.JRestlessHandlerContainer; +import com.jrestless.core.container.io.JRestlessContainerRequest; +import com.jrestless.openwhisk.webaction.io.DefaultWebActionRequest; +import com.jrestless.openwhisk.webaction.io.WebActionRequest; +import com.jrestless.openwhisk.webaction.io.WebActionRequestBuilder; +import com.jrestless.test.IOUtils; + +public class WebActionRequestHandlerTest { + + private static final Type WEB_ACTION_REQUEST_TYPE = (new TypeLiteral>() { }).getType(); + private static final Gson GSON = new GsonBuilder().create(); + + private final WebActionRequestHandler handler = new WebActionRequestHandler() { + @Override + protected JsonObject createJsonResponse(String body, Map responseHeaders, + StatusType statusType) { + return createResponse(body, responseHeaders, statusType); + } + }; + private JRestlessHandlerContainer container; + + @SuppressWarnings("unchecked") + @Before + public void setup() { + container = mock(JRestlessHandlerContainer.class); + handler.init(container); + handler.start(); + } + +// @Test +// public void delegateJsonRequest_NullRequestGiven_ShouldFailWithInternalServerError() { +// JsonObject actualResponse = handler.delegateJsonRequest(null); +// JsonObject expectedResponse = createResponse(null, Collections.emptyMap(), Status.INTERNAL_SERVER_ERROR); +// assertEquals(expectedResponse, actualResponse); +// } +// +// @Test +// public void delegateJsonRequest_RequestWithNoMethodGiven_ShouldFailWithInternalServerError() { +// JsonObject request = new WebActionRequestBuilder().buildJson(); +// JsonObject actualResponse = handler.delegateJsonRequest(request); +// JsonObject expectedResponse = createResponse(null, Collections.emptyMap(), Status.INTERNAL_SERVER_ERROR); +// assertEquals(expectedResponse, actualResponse); +// } +// +// @Test +// public void delegateJsonRequest_MinimalRequestGiven_ShouldDelegateRequest() { +// JsonObject request = new WebActionRequestBuilder() +// .setHttpMethod("GET") +// .buildJson(); +// JsonObject actualResponse = handler.delegateJsonRequest(request); +// JsonObject expectedResponse = createResponse("", Collections.emptyMap(), Status.NOT_FOUND); +// assertEquals(expectedResponse, actualResponse); +// } +// +// @Test +// public void delegateJsonRequest_RequestWithNullHeadersGiven_ShouldDelegateRequest() { +// JsonObject request = new WebActionRequestBuilder() +// .setHttpMethod("GET") +// .setHeaders(null) +// .buildJson(); +// JsonObject actualResponse = handler.delegateJsonRequest(request); +// JsonObject expectedResponse = createResponse("", Collections.emptyMap(), Status.NOT_FOUND); +// assertEquals(expectedResponse, actualResponse); +// } + + @Test(expected = NullPointerException.class) + public void createContainerRequest_NullRequestGiven_ShouldFailWithNpe() { + handler.createContainerRequest(null); + } + + @Test(expected = NullPointerException.class) + public void createContainerRequest_NullMethodGiven_ShouldFailWithNpe() { + WebActionRequest request = minimalRequestBuilder() + .setHttpMethod(null) + .build(); + handler.createContainerRequest(request); + } + + @Test + public void createContainerRequest_NullHeadersGiven_ShouldMapToEmptyHeaders() { + WebActionRequest request = minimalRequestBuilder() + .setHeaders(null) + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertEquals(Collections.emptyMap(), containerRequest.getHeaders()); + } + + @Test + public void createContainerRequest_NullBodyGiven_ShouldMapToEmptyInputStream() { + WebActionRequest request = minimalRequestBuilder() + .setRawBody(null) + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertArrayEquals(new byte[0], IOUtils.toBytes(containerRequest.getEntityStream())); + } + + @Test + public void createContainerRequest_BodyGiven_ShouldMapToInputStream() { + WebActionRequest request = minimalRequestBuilder() + .setRawBody("some body") + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertArrayEquals("some body".getBytes(), IOUtils.toBytes(containerRequest.getEntityStream())); + } + + @Test + public void createContainerRequest_HeadersGiven_ShouldExpandHeaders() { + Map requestHeaders = ImmutableMap.of("hk0", "hv0", "hk1", "hv1"); + WebActionRequest request = minimalRequestBuilder() + .setHeaders(requestHeaders) + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertEquals(ImmutableMap.of("hk0", Collections.singletonList("hv0"), "hk1", Collections.singletonList("hv1")), + containerRequest.getHeaders()); + } + + @Test + public void createContainerRequest_NullPathGiven_ShouldMapToRootRequestPath() { + WebActionRequest request = minimalRequestBuilder() + .setPath(null) + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertEquals(URI.create("/"), containerRequest.getRequestUri()); + assertEquals(URI.create("/"), containerRequest.getBaseUri()); + } + + @Test + public void createContainerRequest_EmptyPathGiven_ShouldMapToRootRequestPath() { + WebActionRequest request = minimalRequestBuilder() + .setPath("") + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertEquals(URI.create("/"), containerRequest.getRequestUri()); + assertEquals(URI.create("/"), containerRequest.getBaseUri()); + } + + @Test + public void createContainerRequest_NonAbsolutePathGiven_ShouldPrependSlashToRequestPath() { + WebActionRequest request = minimalRequestBuilder() + .setPath("path") + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertEquals(URI.create("/path"), containerRequest.getRequestUri()); + assertEquals(URI.create("/"), containerRequest.getBaseUri()); + } + + @Test + public void createContainerRequest_AbsolutePathGiven_ShouldMapRequestPathAsIs() { + WebActionRequest request = minimalRequestBuilder() + .setPath("/path") + .build(); + JRestlessContainerRequest containerRequest = handler.createContainerRequest(request); + assertEquals(URI.create("/path"), containerRequest.getRequestUri()); + assertEquals(URI.create("/"), containerRequest.getBaseUri()); + } + + @Test + public void onRequestFailure_ShouldCreate500() { + JsonObject response = handler.onRequestFailure(null, null, null); + assertEquals(createResponse(null, Collections.emptyMap(), Status.INTERNAL_SERVER_ERROR), response); + } + + @Test + public void delegateJsonRequest_InvalidJsonGiven_ShouldFailWith500() { + JsonObject request = minimalRequestBuilder() + .buildJson(); + request.addProperty("__ow_headers", true); + JsonObject response = handler.delegateJsonRequest(request); + assertEquals(createResponse(null, Collections.emptyMap(), Status.INTERNAL_SERVER_ERROR), response); + } + + @Test + public void delegateJsonRequest_ValidRequestAndReferencesGiven_ShouldSetReferencesOnRequestInitialization() { + + WebActionRequestBuilder requestBuilder = new WebActionRequestBuilder() + .setHttpMethod("GET") + .setPath("/"); + + JsonObject jsonRequest = requestBuilder.buildJson(); + DefaultWebActionRequest request = requestBuilder.build(); + + RequestScopedInitializer requestScopedInitializer = getSetRequestScopedInitializer(jsonRequest); + + @SuppressWarnings("unchecked") + Ref gatewayRequestRef = mock(Ref.class); + + ServiceLocator serviceLocator = mock(ServiceLocator.class); + when(serviceLocator.getService(WEB_ACTION_REQUEST_TYPE)).thenReturn(gatewayRequestRef); + + requestScopedInitializer.initialize(serviceLocator); + + verify(gatewayRequestRef).set(request); + } + + @Test + public void delegateRequest_ValidRequestAndNoReferencesGiven_ShouldNotFailOnRequestInitialization() { + JsonObject jsonRequest = new WebActionRequestBuilder() + .setHttpMethod("GET") + .setPath("/") + .buildJson(); + + RequestScopedInitializer requestScopedInitializer = getSetRequestScopedInitializer(jsonRequest); + + ServiceLocator serviceLocator = mock(ServiceLocator.class); + requestScopedInitializer.initialize(serviceLocator); + } + + @SuppressWarnings("unchecked") + private RequestScopedInitializer getSetRequestScopedInitializer(JsonObject request) { + ArgumentCaptor> containerEnhancerCaptor = ArgumentCaptor.forClass(Consumer.class); + handler.delegateJsonRequest(request); + verify(container).handleRequest(any(), any(), any(), containerEnhancerCaptor.capture()); + + ContainerRequest containerRequest = mock(ContainerRequest.class); + containerEnhancerCaptor.getValue().accept(containerRequest); + + ArgumentCaptor requestScopedInitializerCaptor = ArgumentCaptor.forClass(RequestScopedInitializer.class); + + verify(containerRequest).setRequestScopedInitializer(requestScopedInitializerCaptor.capture()); + + return requestScopedInitializerCaptor.getValue(); + } + + private static JsonObject createResponse(String body, Map headers, StatusType status) { + return createResponse(body, headers, status.getStatusCode()); + } + + private static JsonObject createResponse(String body, Map headers, int statusCode) { + JsonObject response = new JsonObject(); + response.addProperty("body", body); + response.addProperty("statusCode", statusCode); + response.add("headers", GSON.toJsonTree(headers, Map.class)); + response.addProperty("someAdditionalTestProperty", true); + return response; + } + + private static WebActionRequestBuilder minimalRequestBuilder() { + return new WebActionRequestBuilder() + .setHttpMethod("GET"); + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequestTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequestTest.java new file mode 100644 index 0000000..a5cbd45 --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/DefaultWebActionRequestTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.lang.reflect.Constructor; +import java.util.Map; + +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; +import com.jrestless.test.CopyConstructorEqualsTester; + +public class DefaultWebActionRequestTest { + + @Test + public void testEquals() { + new CopyConstructorEqualsTester(getConstructor()) + .addArguments(0, null, "httpMethod") + .addArguments(1, null, ImmutableMap.of("headers", "headers")) + .addArguments(2, null, "path") + .addArguments(3, null, "user") + .addArguments(4, null, "body") + .addArguments(5, null, "query") + .testEquals(); + } + + @Test + public void testGetters() { + Map headers = ImmutableMap.of("headers", "headers"); + DefaultWebActionRequest request = new DefaultWebActionRequest( + "httpMethod", + headers, + "path", + "user", + "body", + "query"); + + assertEquals("httpMethod", request.getHttpMethod()); + assertEquals(headers, request.getHeaders()); + assertEquals("path", request.getPath()); + assertEquals("user", request.getUser()); + assertEquals("body", request.getBody()); + assertEquals("query", request.getQuery()); + } + + @Test + public void testNoArgsConsutructorCreatesEmptyObject() { + DefaultWebActionRequest request = new DefaultWebActionRequest(); + assertNull(request.getHttpMethod()); + assertNull(request.getHeaders()); + assertNull(request.getPath()); + assertNull(request.getUser()); + assertNull(request.getBody()); + assertNull(request.getQuery()); + + } + + private Constructor getConstructor() { + try { + return DefaultWebActionRequest.class.getDeclaredConstructor(String.class, Map.class, String.class, + String.class, String.class, String.class); + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptorTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptorTest.java new file mode 100644 index 0000000..5e7fb4f --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64ReadInterceptorTest.java @@ -0,0 +1,104 @@ +package com.jrestless.openwhisk.webaction.io; + +import static org.junit.Assert.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ReaderInterceptorContext; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.ArgumentCaptor; + +@RunWith(Parameterized.class) +public class WebActionBase64ReadInterceptorTest { + + private final WebActionBase64ReadInterceptor readInterceptor = new WebActionBase64ReadInterceptor(); + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { "text/plain", false }, + { "text/html", false }, + { "application/json", true }, + { "image/png", true }, + { "whatever/whatever", true }, + { null, true } + }); + }; + + private final String contentType; + private final boolean base64Encoded; + + public WebActionBase64ReadInterceptorTest(String contentType, boolean base64Encoded) { + this.contentType = contentType; + this.base64Encoded = base64Encoded; + } + + @Test + public void test() throws WebApplicationException, IOException { + if (base64Encoded) { + testWrapsInputStream(contentType); + } else { + testDoesNotWrapInputStream(contentType); + } + } + + public void testDoesNotWrapInputStream(String contentType) throws WebApplicationException, IOException { + ReaderInterceptorContext context = mockContext(contentType); + InputStream is = mock(InputStream.class); + when(context.getInputStream()).thenReturn(is); + + readInterceptor.aroundReadFrom(context); + + verifyZeroInteractions(is); + + verify(context).getMediaType(); + verify(context).proceed(); + verifyNoMoreInteractions(context); + } + + public void testWrapsInputStream(String contentType) throws WebApplicationException, IOException { + ReaderInterceptorContext context = mockContext(contentType); + InputStream is = mock(InputStream.class); + when(context.getInputStream()).thenReturn(is); + + readInterceptor.aroundReadFrom(context); + + verifyZeroInteractions(is); + + ArgumentCaptor updatedIsCapture = ArgumentCaptor.forClass(InputStream.class); + verify(context).setInputStream(updatedIsCapture.capture()); + verify(context).getMediaType(); + verify(context).getInputStream(); + verify(context).proceed(); + verifyNoMoreInteractions(context); + + InputStream updatedIs = updatedIsCapture.getValue(); + + // just make sure we have some wrapper + assertNotSame(is, updatedIs); + updatedIs.close(); + verify(is).close(); + } + + private static ReaderInterceptorContext mockContext(String contentType) { + ReaderInterceptorContext context = mock(ReaderInterceptorContext.class); + if (contentType != null) { + when(context.getMediaType()).thenReturn(MediaType.valueOf(contentType)); + } + return context; + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptorTest.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptorTest.java new file mode 100644 index 0000000..07f795c --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionBase64WriteInterceptorTest.java @@ -0,0 +1,104 @@ +package com.jrestless.openwhisk.webaction.io; + +import static org.junit.Assert.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.WriterInterceptorContext; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.ArgumentCaptor; + +@RunWith(Parameterized.class) +public class WebActionBase64WriteInterceptorTest { + + private final WebActionBase64WriteInterceptor writeInterceptor = new WebActionBase64WriteInterceptor(); + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { "text/plain", false }, + { "text/html", false }, + { "application/json", true }, + { "image/png", true }, + { "whatever/whatever", true }, + { null, true } + }); + }; + + private final String contentType; + private final boolean base64Encoded; + + public WebActionBase64WriteInterceptorTest(String contentType, boolean base64Encoded) { + this.contentType = contentType; + this.base64Encoded = base64Encoded; + } + + @Test + public void test() throws WebApplicationException, IOException { + if (base64Encoded) { + testWrapsInputStream(contentType); + } else { + testDoesNotWrapInputStream(contentType); + } + } + + public void testDoesNotWrapInputStream(String contentType) throws WebApplicationException, IOException { + WriterInterceptorContext context = mockContext(contentType); + OutputStream os = mock(OutputStream.class); + when(context.getOutputStream()).thenReturn(os); + + writeInterceptor.aroundWriteTo(context); + + verifyZeroInteractions(os); + + verify(context).getMediaType(); + verify(context).proceed(); + verifyNoMoreInteractions(context); + } + + public void testWrapsInputStream(String contentType) throws WebApplicationException, IOException { + WriterInterceptorContext context = mockContext(contentType); + OutputStream os = mock(OutputStream.class); + when(context.getOutputStream()).thenReturn(os); + + writeInterceptor.aroundWriteTo(context); + + verifyZeroInteractions(os); + + ArgumentCaptor updatedOsCapture = ArgumentCaptor.forClass(OutputStream.class); + verify(context).setOutputStream(updatedOsCapture.capture()); + verify(context).getMediaType(); + verify(context).getOutputStream(); + verify(context).proceed(); + verifyNoMoreInteractions(context); + + OutputStream updatedOs = updatedOsCapture.getValue(); + + // just make sure we have some wrapper + assertNotSame(os, updatedOs); + updatedOs.close(); + verify(os).close(); + } + + private static WriterInterceptorContext mockContext(String contentType) { + WriterInterceptorContext context = mock(WriterInterceptorContext.class); + if (contentType != null) { + when(context.getMediaType()).thenReturn(MediaType.valueOf(contentType)); + } + return context; + } +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionHttpResponseBuilder.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionHttpResponseBuilder.java new file mode 100644 index 0000000..ebc2d2a --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionHttpResponseBuilder.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.Response.StatusType; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + +public class WebActionHttpResponseBuilder { + + private static final Gson GSON = new GsonBuilder().create(); + + private int statusCode = Status.OK.getStatusCode(); + private Map headers = new HashMap<>(); + private String body = ""; + + public WebActionHttpResponseBuilder setStatusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public WebActionHttpResponseBuilder setStatusType(StatusType statusType) { + return setStatusCode(statusType.getStatusCode()); + } + + public WebActionHttpResponseBuilder setHeaders(Map headers) { + this.headers = headers; + return this; + } + + public WebActionHttpResponseBuilder addHeader(String name, String value) { + headers.put(name, value); + return this; + } + + public WebActionHttpResponseBuilder setContentType(String contentType) { + return addHeader(HttpHeaders.CONTENT_TYPE, contentType); + } + + public WebActionHttpResponseBuilder setContentType(MediaType contentType) { + return addHeader(HttpHeaders.CONTENT_TYPE, contentType.toString()); + } + + public WebActionHttpResponseBuilder addHeaders(Map headers) { + this.headers.putAll(headers); + return this; + } + + public WebActionHttpResponseBuilder setBody(String body) { + this.body = body; + return this; + } + + public WebActionHttpResponseBuilder setBodyBase64Encoded(String body) { + return setBody(new String(Base64.getEncoder().encode(body.getBytes()), StandardCharsets.UTF_8)); + } + + public JsonObject build() { + JsonObject response = new JsonObject(); + response.addProperty("body", body); + response.addProperty("statusCode", statusCode); + if (headers != null) { + response.add("headers", GSON.toJsonTree(headers, Map.class)); + } + return response; + } + + public static JsonObject noContent() { + return new WebActionHttpResponseBuilder().setStatusType(Status.NO_CONTENT).build(); + } + +} diff --git a/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionRequestBuilder.java b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionRequestBuilder.java new file mode 100644 index 0000000..705efff --- /dev/null +++ b/openwhisk/webaction/jrestless-openwhisk-webaction-handler/src/test/java/com/jrestless/openwhisk/webaction/io/WebActionRequestBuilder.java @@ -0,0 +1,100 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.openwhisk.webaction.io; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + +public class WebActionRequestBuilder { + + private static final Gson GSON = new GsonBuilder().create(); + + private String httpMethod; + private Map headers = new HashMap<>(); + private String path; + private String user; + private String body; + private String query; + + public WebActionRequestBuilder setHttpMethod(String httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + public WebActionRequestBuilder setHeaders(Map headers) { + this.headers = headers; + return this; + } + + public WebActionRequestBuilder addHeader(String name, String value) { + headers.put(name, value); + return this; + } + + public WebActionRequestBuilder setContentType(String contentType) { + return addHeader(HttpHeaders.CONTENT_TYPE, contentType); + } + + public WebActionRequestBuilder setContentType(MediaType contentType) { + return addHeader(HttpHeaders.CONTENT_TYPE, contentType.getType()); + } + + public WebActionRequestBuilder addAllHeaders(Map headers) { + headers.putAll(headers); + return this; + } + + public WebActionRequestBuilder setPath(String path) { + this.path = path; + return this; + } + + public WebActionRequestBuilder setUser(String user) { + this.user = user; + return this; + } + + public WebActionRequestBuilder setRawBody(String body) { + this.body = body; + return this; + } + + public WebActionRequestBuilder setBodyBase64Encoded(String body) { + return setRawBody(new String(Base64.getEncoder().encode(body.getBytes()), StandardCharsets.UTF_8)); + } + + public WebActionRequestBuilder setQuery(String query) { + this.query = query; + return this; + } + + public DefaultWebActionRequest build() { + return new DefaultWebActionRequest(httpMethod, headers, path, user, body, query); + } + + public JsonObject buildJson() { + return (JsonObject) GSON.toJsonTree(build(), DefaultWebActionRequest.class); + } +} diff --git a/settings.gradle b/settings.gradle index a6d61ea..e74a980 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include "test:jrestless-test", 'core:jrestless-core-container', 'core:jrestless-core', 'aws:core:jrestless-aws-core', 'aws:core:jrestless-aws-core-handler', 'aws:gateway:jrestless-aws-gateway-handler', 'aws:service:jrestless-aws-service-handler', 'aws:service:jrestless-aws-service-feign-client', 'aws:sns:jrestless-aws-sns-handler' +include "test:jrestless-test", 'core:jrestless-core-container', 'core:jrestless-core', 'aws:core:jrestless-aws-core', 'aws:core:jrestless-aws-core-handler', 'aws:gateway:jrestless-aws-gateway-handler', 'aws:service:jrestless-aws-service-handler', 'aws:service:jrestless-aws-service-feign-client', 'aws:sns:jrestless-aws-sns-handler', 'openwhisk:core:jrestless-openwhisk-core', 'openwhisk:webaction:jrestless-openwhisk-webaction-handler' rootProject.name = 'jrestless' diff --git a/test/jrestless-test/build.gradle b/test/jrestless-test/build.gradle index f6f5886..6193ef3 100644 --- a/test/jrestless-test/build.gradle +++ b/test/jrestless-test/build.gradle @@ -3,7 +3,9 @@ version = "${globaleModuleVersion}" dependencies { compile( - libraries.guava_testlib + libraries.guava_testlib, + libraries.jersey_test_core, + libraries.jersey_test_grizzly ) testCompile( libraries.junit, diff --git a/test/jrestless-test/src/main/java/com/jrestless/test/DynamicJerseyTestRunner.java b/test/jrestless-test/src/main/java/com/jrestless/test/DynamicJerseyTestRunner.java new file mode 100644 index 0000000..88af7d3 --- /dev/null +++ b/test/jrestless-test/src/main/java/com/jrestless/test/DynamicJerseyTestRunner.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.test; + +import org.glassfish.jersey.test.JerseyTest; + +/** + * Utility class to dynamically invoke a {@link JerseyTest}. + *

+ * The problem with extending {@link JerseyTest} is that the registered + * {@link org.glassfish.jersey.server.ResourceConfig} cannot be changed between tests. + * + * @author Bjoern Bilger + * + */ +public class DynamicJerseyTestRunner { + + /** + *

    + *
  1. calls {@link JerseyTest#setUp()} + *
  2. passes the initialized JerseyTest to the consumer. + *
  3. calls {@link JerseyTest#tearDown()} + *
+ * + * @param jerseyTest + * @param test + * @throws Exception + */ + public void runJerseyTest(JerseyTest jerseyTest, ThrowingConsumer test) throws Exception { + try { + try { + jerseyTest.setUp(); + } catch (Exception e) { + throw new RuntimeException(e); + } + test.accept(jerseyTest); + } finally { + try { + jerseyTest.tearDown(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @FunctionalInterface + public interface ThrowingConsumer { + void accept(T in) throws Exception; + } +} diff --git a/test/jrestless-test/src/main/java/com/jrestless/test/IOUtils.java b/test/jrestless-test/src/main/java/com/jrestless/test/IOUtils.java new file mode 100644 index 0000000..0849b45 --- /dev/null +++ b/test/jrestless-test/src/main/java/com/jrestless/test/IOUtils.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Bjoern Bilger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jrestless.test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * I/O utilities. + * + * @author Bjoern Bilger + * + */ +public final class IOUtils { + + private static final int BUFFER_LENGTH = 1024; + + private IOUtils() { + } + + /** + * Converts the given input stream into a byte array. + *

+ * Possible {@link IOException} are wrapped into a {@link RuntimeException}. + * + * @param is + * the input stream to read from + * @return the bytes read from the input stream + */ + public static byte[] toBytes(InputStream is) { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[BUFFER_LENGTH]; + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + return buffer.toByteArray(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + /** + * Converts the given input stream into a string with UTF8 as charset. + * + * @see #toString(InputStream, Charset) + * @param is + * the input stream to read from + * @return the string created from the given input stream + */ + public static String toString(InputStream is) { + return toString(is, StandardCharsets.UTF_8); + } + + + /** + * Converts the given input stream into a string with the given charset. + * + * @see #toBytes(InputStream) + * @param is + * the input stream to read from + * @param charset + * the charset for the created string + * @return the string created from the given input stream + */ + public static String toString(InputStream is, Charset charset) { + return new String(toBytes(is), charset); + } + +} diff --git a/test/jrestless-test/src/main/java/com/jrestless/test/UtilityClassCodeCoverageBumper.java b/test/jrestless-test/src/main/java/com/jrestless/test/UtilityClassCodeCoverageBumper.java new file mode 100644 index 0000000..07e5e9d --- /dev/null +++ b/test/jrestless-test/src/main/java/com/jrestless/test/UtilityClassCodeCoverageBumper.java @@ -0,0 +1,27 @@ +package com.jrestless.test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Invokes the private no-args constructor of utility classes to bump code coverage. + * + * @author Bjoern Bilger + * + */ +public final class UtilityClassCodeCoverageBumper { + + private UtilityClassCodeCoverageBumper() { + } + + public static void invokePrivateConstructor(Class clazz) { + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + constructor.newInstance(); + } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} diff --git a/test/jrestless-test/src/test/java/com/jrestless/test/AccessibleRunnerTest.java b/test/jrestless-test/src/test/java/com/jrestless/test/AccessibleRunnerTest.java index 9b0bd01..d51c54f 100644 --- a/test/jrestless-test/src/test/java/com/jrestless/test/AccessibleRunnerTest.java +++ b/test/jrestless-test/src/test/java/com/jrestless/test/AccessibleRunnerTest.java @@ -74,6 +74,11 @@ public void run_AccessibleAoAndThrowingSupplierGiven_ShouldKeepAoAccessibleAfter } } + @Test + public void bumpCodeCoverageByInvokingThePrivateConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(AccessibleRunner.class); + } + public static class SomeObject { @SuppressWarnings("unused") private void method() { diff --git a/test/jrestless-test/src/test/java/com/jrestless/test/ConstructorPreconditionsTesterTest.java b/test/jrestless-test/src/test/java/com/jrestless/test/ConstructorPreconditionsTesterTest.java index 1b3b969..1c2d054 100644 --- a/test/jrestless-test/src/test/java/com/jrestless/test/ConstructorPreconditionsTesterTest.java +++ b/test/jrestless-test/src/test/java/com/jrestless/test/ConstructorPreconditionsTesterTest.java @@ -202,6 +202,15 @@ public void addInvalidArgs_IncompatibleTypeGiven_ShouldThrowIae() { tester.addInvalidArgs(0, NullPointerException.class, 1L); } + @Test(expected = AssertionError.class) + public void addInvalidArgs_IncorrectExceptionGiven_ShouldThrowAssertionError() { + tester + .addInvalidArgs(0, IllegalArgumentException.class, new Object[] { null }) + .addValidArgs(0, 1) + .addValidArgs(1, 1.0) + .testPreconditions(); + } + private static class SomeClassCapture { private final Integer a; private final double b; diff --git a/test/jrestless-test/src/test/java/com/jrestless/test/DynamicJerseyTestRunnerTest.java b/test/jrestless-test/src/test/java/com/jrestless/test/DynamicJerseyTestRunnerTest.java new file mode 100644 index 0000000..11ba15e --- /dev/null +++ b/test/jrestless-test/src/test/java/com/jrestless/test/DynamicJerseyTestRunnerTest.java @@ -0,0 +1,123 @@ +package com.jrestless.test; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import com.jrestless.test.DynamicJerseyTestRunner.ThrowingConsumer; + +public class DynamicJerseyTestRunnerTest { + + private DynamicJerseyTestRunner runner = new DynamicJerseyTestRunner(); + private final JerseyTest jerseyTest = mock(JerseyTest.class); + @SuppressWarnings("unchecked") + private final ThrowingConsumer consumer = mock(ThrowingConsumer.class); + + @Test + public void testTearsDownTestOnSetupFailure() throws Exception { + doThrow(new RuntimeException()).when(jerseyTest).setUp(); + try { + runner.runJerseyTest(jerseyTest, consumer); + fail("expected exception to be thrown"); + } catch (RuntimeException re) { + } + verify(jerseyTest).setUp(); + verify(jerseyTest).tearDown(); + } + + @Test + public void testRethrowsExceptionOnStartupFailure() throws Exception { + RuntimeException thrownException = new RuntimeException("whatever"); + doThrow(thrownException).when(jerseyTest).setUp(); + try { + runner.runJerseyTest(jerseyTest, consumer); + fail("expected exception to be thrown"); + } catch (RuntimeException re) { + assertSame(thrownException, re.getCause()); + } + } + + @Test + public void testDoesNotCallConsumerOnStartupFailure() throws Exception { + doThrow(new RuntimeException()).when(jerseyTest).setUp(); + try { + runner.runJerseyTest(jerseyTest, consumer); + fail("expected exception to be thrown"); + } catch (RuntimeException re) { + } + verifyZeroInteractions(consumer);; + } + + @Test + public void testRethrowsExceptionOnTearDownFailure() throws Exception { + RuntimeException thrownException = new RuntimeException("whatever"); + doThrow(thrownException).when(jerseyTest).tearDown(); + try { + runner.runJerseyTest(jerseyTest, consumer); + fail("expected exception to be thrown"); + } catch (RuntimeException re) { + assertSame(thrownException, re.getCause()); + } + } + + @Test + public void testCallSetupConsumerAndTearDownInOrder() throws Exception { + runner.runJerseyTest(jerseyTest, consumer); + InOrder inOrder = Mockito.inOrder(jerseyTest, consumer); + inOrder.verify(jerseyTest).setUp(); + inOrder.verify(consumer).accept(jerseyTest); + inOrder.verify(jerseyTest).tearDown(); + } + + @Test + public void testPassesJerseyTestToConsumer() throws Exception { + runner.runJerseyTest(jerseyTest, consumer); + verify(consumer).accept(jerseyTest); + } + + @Test + public void testCallsTearDownOnConsumerException() throws Exception { + doThrow(new Exception("whatever")).when(consumer).accept(jerseyTest); + try { + runner.runJerseyTest(jerseyTest, consumer); + fail("expected exception to be thrown"); + } catch (Exception e) { + ; + } + verify(jerseyTest).tearDown(); + } + + @Test + public void testRethrowsExceptionOnConsumerFailure() throws Exception { + Exception thrownException = new Exception("whatever"); + doThrow(thrownException).when(consumer).accept(jerseyTest); + try { + runner.runJerseyTest(jerseyTest, consumer); + fail("expected exception to be thrown"); + } catch (Exception e) { + assertSame(thrownException, e); + } + } + + @Test + public void testRethrowsTearDownExceptionOnConsumerAndTearDownFailure() throws Exception { + Exception consumerException = new Exception("whatever"); + doThrow(consumerException).when(consumer).accept(jerseyTest); + RuntimeException tearDownException = new RuntimeException("whatever"); + doThrow(tearDownException).when(jerseyTest).tearDown(); + try { + runner.runJerseyTest(jerseyTest, consumer); + fail("expected exception to be thrown"); + } catch (Exception e) { + assertSame(tearDownException, e.getCause()); + } + } +} diff --git a/test/jrestless-test/src/test/java/com/jrestless/test/IOUtilsTest.java b/test/jrestless-test/src/test/java/com/jrestless/test/IOUtilsTest.java new file mode 100644 index 0000000..e068596 --- /dev/null +++ b/test/jrestless-test/src/test/java/com/jrestless/test/IOUtilsTest.java @@ -0,0 +1,76 @@ +package com.jrestless.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +public class IOUtilsTest { + + private static final String UTF8_SYMBOL = "☺"; + + @Test + public void testToBytes() throws IOException { + assertArrayEquals("test".getBytes(), IOUtils.toBytes(new ByteArrayInputStream("test".getBytes()))); + } + + @Test + public void testToBytesWithEmptyByteArray() throws IOException { + assertArrayEquals(new byte[0], IOUtils.toBytes(new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testToBytesRethrowsIOExceptionWrapped() throws IOException { + InputStream is = mock(InputStream.class); + IOException thrownException = new IOException("whatever"); + when(is.read(any(), anyInt(), anyInt())).thenThrow(thrownException); + try { + IOUtils.toBytes(is); + fail("expected exception to be thrown"); + } catch (RuntimeException re) { + assertSame(thrownException, re.getCause()); + } + } + + @Test + public void testToBytesRethrowsRuntimeExceptionAsIs() throws IOException { + InputStream is = mock(InputStream.class); + RuntimeException thrownException = new RuntimeException("whatever"); + when(is.read(any(), anyInt(), anyInt())).thenThrow(thrownException); + try { + IOUtils.toBytes(is); + fail("expected exception to be thrown"); + } catch (RuntimeException re) { + assertSame(thrownException, re); + } + } + + @Test + public void testToStringUsesUtf8() { + assertEquals(UTF8_SYMBOL, IOUtils.toString(new ByteArrayInputStream(UTF8_SYMBOL.getBytes()))); + } + + @Test + public void testToStringCharsets() { + assertEquals(UTF8_SYMBOL, IOUtils.toString(new ByteArrayInputStream(UTF8_SYMBOL.getBytes()))); + assertNotEquals(UTF8_SYMBOL, + IOUtils.toString(new ByteArrayInputStream(UTF8_SYMBOL.getBytes()), StandardCharsets.ISO_8859_1)); + } + + @Test + public void bumpCodeCoverageByInvokingThePrivateConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(IOUtils.class); + } +} diff --git a/test/jrestless-test/src/test/java/com/jrestless/test/InvokableArgumentsArgumentTest.java b/test/jrestless-test/src/test/java/com/jrestless/test/InvokableArgumentsArgumentTest.java new file mode 100644 index 0000000..5be45fb --- /dev/null +++ b/test/jrestless-test/src/test/java/com/jrestless/test/InvokableArgumentsArgumentTest.java @@ -0,0 +1,23 @@ +package com.jrestless.test; + +import java.lang.reflect.Constructor; + +import org.junit.Test; + +public class InvokableArgumentsArgumentTest { + + @Test + public void testEquals() { + new CopyConstructorEqualsTester(getConstructor()) + .addArguments(0, null, "a", 1, 2) + .testEquals(); + } + + private Constructor getConstructor() { + try { + return InvokableArguments.Argument.class.getDeclaredConstructor(Object.class); + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } +} diff --git a/test/jrestless-test/src/test/java/com/jrestless/test/UtilityClassCodeCoverageBumperTest.java b/test/jrestless-test/src/test/java/com/jrestless/test/UtilityClassCodeCoverageBumperTest.java new file mode 100644 index 0000000..755f84d --- /dev/null +++ b/test/jrestless-test/src/test/java/com/jrestless/test/UtilityClassCodeCoverageBumperTest.java @@ -0,0 +1,39 @@ +package com.jrestless.test; + +import org.junit.Test; + +public class UtilityClassCodeCoverageBumperTest { + + @Test + public void testInvokesPublicNoArgsConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(PublicNoArgs.class); + } + + @Test + public void testInvokesPrivateNoArgsConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(PrivateNoArgs.class); + } + + @Test(expected = RuntimeException.class) + public void testFailsToInvokeClassWithNoNoArgsConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(NoNoArgs.class); + } + + @Test + public void bumpCodeCoverageByInvokingThePrivateConstructor() { + UtilityClassCodeCoverageBumper.invokePrivateConstructor(UtilityClassCodeCoverageBumper.class); + } + + private static class PublicNoArgs { + } + + private static class PrivateNoArgs { + private PrivateNoArgs() { + } + } + + private static class NoNoArgs { + public NoNoArgs(String x) { + } + } +}