You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2020/11/12 20:25:21 UTC

[isis] branch master updated: ISIS-2454: rewriting CorsFilterTest from scratch, not using any mockery

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new 4e2e835  ISIS-2454: rewriting CorsFilterTest from scratch, not using any mockery
4e2e835 is described below

commit 4e2e835697cde6bc73d196b9c8b1a9c1c64fbf04
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Nov 12 21:25:09 2020 +0100

    ISIS-2454: rewriting CorsFilterTest from scratch, not using any mockery
---
 .../cors/impl/IsisModuleExtCorsImpl.java           |  19 +-
 .../cors/impl/IsisModuleExtCorsImplTest.java       |   4 +-
 .../restclient/log/ClientConversationFilter.java   |   2 +-
 .../RestServiceSimpifiedRepresentationTest.java    |  36 ++-
 .../isis/testdomain/rospec/RoSpecSampler.java      |   7 +-
 .../isis/testdomain/rest/CorsFilterTest.java       | 268 ++++++++++-----------
 6 files changed, 181 insertions(+), 155 deletions(-)

diff --git a/extensions/vro/cors/impl/src/main/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImpl.java b/extensions/vro/cors/impl/src/main/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImpl.java
index 221fc3e..c050bfb 100644
--- a/extensions/vro/cors/impl/src/main/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImpl.java
+++ b/extensions/vro/cors/impl/src/main/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImpl.java
@@ -21,7 +21,6 @@ package org.apache.isis.extensions.cors.impl;
 import java.util.Collections;
 import java.util.Map;
 
-import javax.inject.Inject;
 import javax.inject.Named;
 import javax.servlet.Filter;
 
@@ -36,39 +35,37 @@ import org.springframework.web.filter.CorsFilter;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.core.config.IsisConfiguration;
 
-import lombok.RequiredArgsConstructor;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
 @Configuration
 @Named("isisMapCors.WebModuleServerCors")
 @Qualifier("CORS")
-@RequiredArgsConstructor(onConstructor_ = {@Inject})
 @Log4j2
 public class IsisModuleExtCorsImpl {
     
-    private final IsisConfiguration configuration;
-
     @Bean
-    public FilterRegistrationBean<Filter> corsFilterRegistration() {
+    public FilterRegistrationBean<Filter> createCorsFilterRegistration(IsisConfiguration configuration) {
 
         final Map<String, String> cfgMap = configuration.getAsMap();
         final String resteasyBase = cfgMap.getOrDefault("resteasy.jaxrs.defaultPath", "/restful/*");
-        log.info("Setting up CORS to filter resteasy-base at '{}'", resteasyBase);
+        log.info("Setting up CORS to filter resteasy-base at '{}' with {}", 
+                resteasyBase, 
+                configuration.getExtensions().getCors());
 
         final FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
-        filterRegistrationBean.setFilter(createCorsFilter());
+        filterRegistrationBean.setFilter(createCorsFilter(configuration));
         filterRegistrationBean.setUrlPatterns(Collections.singletonList(resteasyBase));
         filterRegistrationBean.setOrder(OrderPrecedence.EARLY - 100);
         return filterRegistrationBean;
     }
 
-    public CorsFilter createCorsFilter() {
+    private CorsFilter createCorsFilter(IsisConfiguration configuration) {
+        
+        val isisCorsConfig = configuration.getExtensions().getCors();
         
         val corsConfiguration = new CorsConfiguration();
         corsConfiguration.setAllowCredentials(true);
-        
-        val isisCorsConfig = configuration.getExtensions().getCors();
         corsConfiguration.setAllowedHeaders(isisCorsConfig.getAllowedHeaders());
         corsConfiguration.setAllowedMethods(isisCorsConfig.getAllowedMethods());
         corsConfiguration.setAllowedOrigins(isisCorsConfig.getAllowedOrigins());
diff --git a/extensions/vro/cors/impl/src/test/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImplTest.java b/extensions/vro/cors/impl/src/test/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImplTest.java
index 130abc0..4d213eb 100644
--- a/extensions/vro/cors/impl/src/test/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImplTest.java
+++ b/extensions/vro/cors/impl/src/test/java/org/apache/isis/extensions/cors/impl/IsisModuleExtCorsImplTest.java
@@ -42,10 +42,10 @@ class IsisModuleExtCorsImplTest {
     @Test
     void defaultIsisConfiguration_shouldYieldCorsUrlPatternWithWildcard() {
         // given
-        val isisModuleExtCorsImpl = new IsisModuleExtCorsImpl(isisDefaultConfiguration);
+        val isisModuleExtCorsImpl = new IsisModuleExtCorsImpl();
         
         // when
-        val filterRegistration = isisModuleExtCorsImpl.corsFilterRegistration();
+        val filterRegistration = isisModuleExtCorsImpl.createCorsFilterRegistration(isisDefaultConfiguration);
         
         // then
         assertTrue(filterRegistration.getUrlPatterns().contains("/restful/*"));
diff --git a/mappings/restclient/applib/src/main/java/org/apache/isis/extensions/restclient/log/ClientConversationFilter.java b/mappings/restclient/applib/src/main/java/org/apache/isis/extensions/restclient/log/ClientConversationFilter.java
index dade8b1..59c0a80 100644
--- a/mappings/restclient/applib/src/main/java/org/apache/isis/extensions/restclient/log/ClientConversationFilter.java
+++ b/mappings/restclient/applib/src/main/java/org/apache/isis/extensions/restclient/log/ClientConversationFilter.java
@@ -62,7 +62,7 @@ extends ClientRequestFilter, ClientResponseFilter {
                 ? "Failed to parse accept header, cause: " + acceptableMediaTypeParsingFailure.getMessage()
                 : "OK";
 
-        final String requestBody = requestContext.getEntity().toString();
+        final String requestBody = "" + requestContext.getEntity();
 
         onRequest(
                 endpoint, method, acceptHeaderParsing,
diff --git a/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/rest/RestServiceSimpifiedRepresentationTest.java b/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/rest/RestServiceSimpifiedRepresentationTest.java
index 10172de..4f84b71 100644
--- a/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/rest/RestServiceSimpifiedRepresentationTest.java
+++ b/regressiontests/incubating/src/test/java/org/apache/isis/testdomain/rest/RestServiceSimpifiedRepresentationTest.java
@@ -109,7 +109,7 @@ class RestServiceSimpifiedRepresentationTest {
         System.out.println("==================================================================");
     }
 
-    // -- STRING
+    // -- VOID
 
     @Test @Order(1) 
     void voidResult() {
@@ -127,6 +127,14 @@ class RestServiceSimpifiedRepresentationTest {
         assertEquals(refSampler.string(), returnValue);
         filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
     }
+    
+    @Test @Order(2) 
+    void stringUsingGet() {
+        val digest = digestUsingGet("stringSafe", String.class);
+        val returnValue = digest.getEntities().getSingletonOrFail();
+        assertEquals(refSampler.stringSafe(), returnValue);
+        filter4ReprType.assertRepresentationType(RepresentationTypeSimplifiedV2.VALUE);
+    }
 
     @Test @Order(3)
     void stringNull() {
@@ -357,6 +365,32 @@ class RestServiceSimpifiedRepresentationTest {
         return digest;
 
     }
+    
+    <T> ResponseDigest<T> digestUsingGet(String actionName, Class<T> entityType) {
+
+        _Probe.errOut("");
+        _Probe.errOut("=== %s", actionName);
+        _Probe.errOut("");
+        
+        assertTrue(restService.getPort()>0);
+
+        val useRequestDebugLogging = false;
+        val client = restService.newClient(useRequestDebugLogging, conversationFilters);
+
+        val request = restService.newInvocationBuilder(client, 
+                String.format("services/testdomain.RoSpecSampler/actions/%s/invoke", actionName)); 
+
+        filter4Reporting.next(String.format("@Action\n%s %s {\n    /*...*/\n}", entityType.getSimpleName(), actionName));
+
+        val response = request.get();
+        val digest = client.digest(response, entityType);
+
+        if(!digest.isSuccess()) {
+            fail(digest.getFailureCause());
+        }
+
+        return digest;
+    }
 
     <T> ResponseDigest<T> digestArray(
             String actionName, 
diff --git a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/rospec/RoSpecSampler.java b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/rospec/RoSpecSampler.java
index 5643c40..4bd98fd 100644
--- a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/rospec/RoSpecSampler.java
+++ b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/rospec/RoSpecSampler.java
@@ -24,6 +24,7 @@ import java.util.List;
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.SemanticsOf;
 import org.apache.isis.commons.internal.collections._Lists;
 
 @DomainService(
@@ -35,7 +36,6 @@ public class RoSpecSampler {
     
     @Action 
     public void voidResult() {
-        
     }
     
     // -- STRING
@@ -45,6 +45,11 @@ public class RoSpecSampler {
         return "aString";
     }
     
+    @Action(semantics = SemanticsOf.SAFE) 
+    public String stringSafe() {
+        return "aSafeString";
+    }
+    
     @Action 
     public String stringNull() {
         return null;
diff --git a/regressiontests/stable/src/test/java/org/apache/isis/testdomain/rest/CorsFilterTest.java b/regressiontests/stable/src/test/java/org/apache/isis/testdomain/rest/CorsFilterTest.java
index 05388d5..ec3bda0 100644
--- a/regressiontests/stable/src/test/java/org/apache/isis/testdomain/rest/CorsFilterTest.java
+++ b/regressiontests/stable/src/test/java/org/apache/isis/testdomain/rest/CorsFilterTest.java
@@ -1,185 +1,175 @@
 /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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
+ *        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.
+ *  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 org.apache.isis.testdomain.rest;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
+import java.util.function.UnaryOperator;
 
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
+import javax.inject.Inject;
 
-import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.junit.jupiter.api.TestMethodOrder;
 import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.web.client.TestRestTemplate;
 import org.springframework.boot.web.server.LocalServerPort;
 import org.springframework.context.annotation.Import;
-import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.mock.web.MockFilterChain;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.test.context.ActiveProfiles;
-import org.springframework.test.context.junit4.SpringRunner;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.RequestBuilder;
-import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
-import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-import org.springframework.web.context.WebApplicationContext;
-import org.springframework.web.filter.CorsFilter;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
+import org.springframework.test.context.TestPropertySource;
+
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.junit.jupiter.api.Assertions.fail;
 
-import org.apache.isis.core.config.IsisConfiguration;
-import org.apache.isis.core.metamodel._testing.MetaModelContext_forTesting;
-import org.apache.isis.core.metamodel.context.MetaModelContext;
+import org.apache.isis.commons.collections.Can;
+import org.apache.isis.core.config.presets.IsisPresets;
 import org.apache.isis.extensions.cors.impl.IsisModuleExtCorsImpl;
+import org.apache.isis.extensions.restclient.ResponseDigest;
+import org.apache.isis.extensions.restclient.log.ClientConversationFilter;
 import org.apache.isis.testdomain.conf.Configuration_headless;
 import org.apache.isis.testdomain.rospec.Configuration_usingRoSpec;
+import org.apache.isis.testdomain.rospec.RoSpecSampler;
 import org.apache.isis.testdomain.util.rest.RestEndpointService;
 import org.apache.isis.viewer.restfulobjects.jaxrsresteasy4.IsisModuleViewerRestfulObjectsJaxrsResteasy4;
 
-@ActiveProfiles("test")
-@RunWith(SpringRunner.class)
+import lombok.val;
+
 @SpringBootTest(
-        classes = {RestEndpointService.class, IsisModuleExtCorsImpl.class},
+        classes = {
+                RestEndpointService.class
+        },
+        properties = {
+                //preparing the CORS filter with specific settings for testing
+                "isis.extensions.cors.allowedMethods=POST",
+                "isis.extensions.cors.allowedOrigins=http://www.google.com",
+        },
         webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@TestPropertySource(IsisPresets.UseLog4j2Test)
 @Import({
-        Configuration_headless.class,
-        Configuration_usingRoSpec.class,
-        IsisModuleViewerRestfulObjectsJaxrsResteasy4.class,
+    Configuration_headless.class,
+    Configuration_usingRoSpec.class,
+    IsisModuleViewerRestfulObjectsJaxrsResteasy4.class,
+    IsisModuleExtCorsImpl.class
 })
-// see: https://github.com/predix/spring-cors-filter/blob/master/src/test/java/com/ge/predix/web/cors/test/CORSFilterTest.java
-// see: https://www.baeldung.com/intercepting-filter-pattern-in-java
-@Disabled
+@TestMethodOrder(OrderAnnotation.class) // run tests in sequence, to ease debugging
 class CorsFilterTest {
 
-    @LocalServerPort
-    private int port;
-
-    @Test
-    // FALSE_POSITIVE:
-    // regardless what is set in IsisModuleExtCorsIml ("restful" vs. "restful/*)
-    // a (sensible) json response is returned
-    public void ensureUrlCanBeAccessed() throws Exception{
-        final String url = "http://localhost:" + port + "/restful/user";
-        final TestRestTemplate testRestTemplate = new TestRestTemplate();
-        final ResponseEntity<String> response = testRestTemplate
-                //Credentials are not checked, only occurrence of HTTP/BA field in request header
-                .withBasicAuth("any", "any")
-                .getForEntity(url, String.class);
-        assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));
-        assertTrue(response.getBody().length() > 0);
-    }
+    @LocalServerPort int port; 
+    @Inject RestEndpointService restService;
 
-    @Autowired
-    private WebApplicationContext wac;
-
-    @Test
-    public void testCors() throws Exception {
-        final RequestBuilder requestBuilder = MockMvcRequestBuilders
-                .get("http://localhost:" + port + "/restful/user")
-                .header("Access-Control-Request-Method", "GET")
-                .header("Origin", "http://localhost");
-        final CorsFilter corsFilter = createIsisCorsFilter();
-        final DefaultMockMvcBuilder builder = MockMvcBuilders
-                .webAppContextSetup(this.wac)
-                .addFilter(corsFilter, "/**");
-        final MockMvc mockMvc = builder.build();
-        mockMvc
-                .perform(requestBuilder)
-                .andDo(print())
-                //ServletResource not available.  //Set BP in PathResourceResolver.getResource:186
-                .andExpect(status().isOk())
-                .andExpect(header().string("Access-Control-Allow-Methods", "GET,PUT,DELETE,POST,OPTIONS"));
-    }
+    private final RoSpecSampler refSampler = new RoSpecSampler();
+    private final Can<ClientConversationFilter> conversationFilters = Can.empty();
+    
+    // -- STRING
 
-    private CorsFilter createIsisCorsFilter() {
-        final MetaModelContext metaModelContext = MetaModelContext_forTesting.buildDefault();
-        final ConfigurableEnvironment environment = metaModelContext.getConfiguration().getEnvironment();
-        final IsisConfiguration configuration = new IsisConfiguration(environment);
-        final Map<String, String> map = new HashMap<>();
+    @Test @Order(1) 
+    void requestWithValidOriginAndMethod_shouldSucceed() {
+        val digest = digestUsingPost("string", String.class, builder->builder
+                .header("Origin", validOrigin()));
+        assertHttpResponse200(digest);
+    }
 
-        configuration.setIsisSettings(map);
+    @Test @Order(2) 
+    void requestWithInvalidOrigin_shouldFail() {
+        val digest = digestUsingPost("string", String.class, builder->builder
+                .header("Origin", invalidOrigin()));
+        assertHttpResponse403(digest);
+    }
+    
+    @Test @Order(3) 
+    void requestWithMissingOrigin_shouldSucceed() {
+        val digest = digestUsingPost("string", String.class, builder->builder);
+        assertHttpResponse200(digest);
+    }
 
-        final IsisModuleExtCorsImpl isisExtCors = new IsisModuleExtCorsImpl(configuration);
-        isisExtCors.corsFilterRegistration();
-        return isisExtCors.createCorsFilter();
+    @Test @Order(4) //@Disabled("not sure why the ")
+    void requestWithInvalidMethod_shouldFail() {
+        val digest = digestUsingGet("stringSafe", String.class, builder->builder
+                .header("Origin", validOrigin()));
+        assertHttpResponse403(digest);
     }
 
-    @Test
-    public void testRequestExpectStandardCorsResponse() throws ServletException, IOException {
-        final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/invalid");
-        request.addHeader("Origin", "*");
+    // -- HELPER
 
-        final MockHttpServletResponse response = new MockHttpServletResponse();
-        final FilterChain filterChain = new MockFilterChain();
-        final Filter corsFilter = createIsisCorsFilter();
-        corsFilter.doFilter(request, response, filterChain);
+    <T> ResponseDigest<T> digestUsingPost(
+            String actionName, 
+            Class<T> entityType, 
+            UnaryOperator<javax.ws.rs.client.Invocation.Builder> onRequestBuilder) {
 
-        assertEquals("*", response.getHeaderValue("Access-Control-Allow-Origin"));
-    }
+        assertTrue(restService.getPort()>0);
 
-    @Test //requestUrl is not seen as invalid. Why?
-    // BP -> CorsFilter.doFilterInternal:87
-    public void testRequestWithForbiddenUri() throws ServletException, IOException {
-        final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/invalid");
-        request.addHeader("Origin", "*");
+        val useRequestDebugLogging = true;
+        val client = restService.newClient(useRequestDebugLogging, conversationFilters);
 
-        final MockHttpServletResponse response = new MockHttpServletResponse();
-        final Filter corsFilter = createIsisCorsFilter();
-        final FilterChain filterChain = new FilterChainImpl(corsFilter);
-        corsFilter.doFilter(request, response, filterChain);
+        val request = onRequestBuilder.apply( 
+                restService.newInvocationBuilder(client, 
+                        String.format("services/testdomain.RoSpecSampler/actions/%s/invoke", actionName)));
+        
+        val args = client.arguments() 
+                .build();
+        
+        val response = request.post(args);
+        val digest = client.digest(response, entityType);
+
+        return digest;
 
-        assertEquals(403, response.getStatus());
     }
+    
+    <T> ResponseDigest<T> digestUsingGet(
+            String actionName, 
+            Class<T> entityType, 
+            UnaryOperator<javax.ws.rs.client.Invocation.Builder> onRequestBuilder) {
 
-    class FilterChainImpl implements FilterChain {
-        private Iterator<Filter> filters;
+        assertTrue(restService.getPort()>0);
 
-        public FilterChainImpl(Filter... filters) {
-            this.filters = Arrays.asList(filters).iterator();
-        }
+        val useRequestDebugLogging = true;
+        val client = restService.newClient(useRequestDebugLogging, conversationFilters);
+
+        val request = onRequestBuilder.apply( 
+                restService.newInvocationBuilder(client, 
+                        String.format("services/testdomain.RoSpecSampler/actions/%s/invoke", actionName)));
+
+        val response = request.get();
+        val digest = client.digest(response, entityType);
 
-        @Override
-        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
-            if (filters.hasNext()) {
-                Filter filter = filters.next();
-                filter.doFilter(request, response, this);
-            }
+        return digest;
+    }
+    
+    
+    private String validOrigin() {
+        return "http://www.google.com";
+    }
+    
+    private String invalidOrigin() {
+        return "http://localhost";
+    }
+   
+    private void assertHttpResponse200(ResponseDigest<String> digest) {
+        if(!digest.isSuccess()) {
+            fail(digest.getFailureCause());
         }
+        val returnValue = digest.getEntities().getSingletonOrFail();
+        assertEquals(refSampler.string(), returnValue);
+    }
+    
+    private void assertHttpResponse403(ResponseDigest<String> digest) {
+        assertNotNull(digest.getFailureCause(), "request was expected to fail, but succeeded");
+        assertTrue(digest.getFailureCause().getMessage().contains("403"));
     }
 
 }