This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5626-cleanup in repository https://gitbox.apache.org/repos/asf/struts.git
commit 3b01a61248117b2e00d767690425ddc86062b62d Author: Lukasz Lenart <[email protected]> AuthorDate: Mon May 4 12:53:45 2026 +0200 WW-5626 add real JacksonJsonHandler integration tests for @StrutsParameter filtering The existing ContentTypeInterceptorTest uses mock ContentTypeHandlers, so its requireAnnotations=true tests verify only that intercept() returns SUCCESS — they assert nothing about which properties were actually filtered. These integration tests use a real JacksonJsonHandler + a real StrutsParameterAuthorizer to verify end-to-end property-level filtering for top-level annotated/unannotated properties and nested properties at varying authorized depths. The SecureRestAction fixture documents a semantic divergence: REST's recursive copy authorizes each path level independently, so depth-0 authorization on the top-level property requires @StrutsParameter on the setter even when nested field access is the actual goal. ParametersInterceptor only requires the getter annotation. This divergence is tracked for the Approach C refactor. --- .../ContentTypeInterceptorIntegrationTest.java | 134 +++++++++++++++++++++ .../org/apache/struts2/rest/SecureRestAction.java | 70 +++++++++++ 2 files changed, 204 insertions(+) diff --git a/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java new file mode 100644 index 000000000..dc11c6fdf --- /dev/null +++ b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java @@ -0,0 +1,134 @@ +/* + * 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 + * + * 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.struts2.rest; + +import com.mockobjects.dynamic.AnyConstraintMatcher; +import com.mockobjects.dynamic.Mock; +import junit.framework.TestCase; +import org.apache.struts2.ActionContext; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.action.Action; +import org.apache.struts2.dispatcher.mapper.ActionMapping; +import org.apache.struts2.interceptor.parameter.StrutsParameterAuthorizer; +import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory; +import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory; +import org.apache.struts2.ognl.OgnlUtil; +import org.apache.struts2.ognl.StrutsOgnlGuard; +import org.apache.struts2.ognl.StrutsProxyCacheFactory; +import org.apache.struts2.rest.handler.JacksonJsonHandler; +import org.apache.struts2.util.StrutsProxyService; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.LRU; + +/** + * Integration tests for ContentTypeInterceptor that use a real {@link JacksonJsonHandler} + * and a real {@link StrutsParameterAuthorizer}, end-to-end. Verifies that property + * filtering actually occurs on the deserialized object — not merely that the wiring runs. + */ +public class ContentTypeInterceptorIntegrationTest extends TestCase { + + private ContentTypeInterceptor interceptor; + private SecureRestAction action; + private Mock mockActionInvocation; + private Mock mockSelector; + + @Override + protected void setUp() throws Exception { + super.setUp(); + action = new SecureRestAction(); + + var ognlUtil = new OgnlUtil( + new DefaultOgnlExpressionCacheFactory<>("1000", LRU.toString()), + new DefaultOgnlBeanInfoCacheFactory<>("1000", LRU.toString()), + new StrutsOgnlGuard()); + var proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); + + StrutsParameterAuthorizer authorizer = new StrutsParameterAuthorizer(); + authorizer.setOgnlUtil(ognlUtil); + authorizer.setProxyService(proxyService); + authorizer.setRequireAnnotations(Boolean.TRUE.toString()); + + interceptor = new ContentTypeInterceptor(); + interceptor.setParameterAuthorizer(authorizer); + interceptor.setRequireAnnotations(Boolean.TRUE.toString()); + + mockActionInvocation = new Mock(ActionInvocation.class); + mockSelector = new Mock(ContentTypeHandlerManager.class); + // ContentTypeInterceptor calls getAction() twice when requireAnnotations=true + mockActionInvocation.expectAndReturn("getAction", action); + mockActionInvocation.expectAndReturn("getAction", action); + mockActionInvocation.expectAndReturn("invoke", Action.SUCCESS); + mockSelector.expectAndReturn("getHandlerForRequest", new AnyConstraintMatcher() { + public boolean matches(Object[] args) { return true; } + }, new JacksonJsonHandler()); + interceptor.setContentTypeHandlerSelector((ContentTypeHandlerManager) mockSelector.proxy()); + } + + private void runWithBody(String body) throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setContent(body.getBytes()); + request.setContentType("application/json"); + + ActionContext.of() + .withActionMapping(new ActionMapping()) + .withServletRequest(request) + .bind(); + + interceptor.intercept((ActionInvocation) mockActionInvocation.proxy()); + mockSelector.verify(); + mockActionInvocation.verify(); + } + + public void testAnnotatedTopLevelPropertyIsApplied() throws Exception { + runWithBody("{\"name\":\"alice\"}"); + assertEquals("alice", action.getName()); + } + + public void testUnannotatedTopLevelPropertyIsRejected() throws Exception { + runWithBody("{\"role\":\"admin\"}"); + assertNull("unannotated 'role' must not be set", action.getRole()); + } + + public void testMixedPropertiesFilteredCorrectly() throws Exception { + runWithBody("{\"name\":\"alice\",\"role\":\"admin\"}"); + assertEquals("alice", action.getName()); + assertNull(action.getRole()); + } + + public void testNestedPropertyAuthorizedWhenDepthAllows() throws Exception { + runWithBody("{\"address\":{\"city\":\"Warsaw\",\"zip\":\"00-001\"}}"); + assertNotNull("address should be set", action.getAddress()); + assertEquals("Warsaw", action.getAddress().getCity()); + assertEquals("00-001", action.getAddress().getZip()); + } + + public void testNestedPropertyRejectedWhenDepthInsufficient() throws Exception { + // shallowAddress has @StrutsParameter (depth=0) — its nested fields should NOT be set + runWithBody("{\"shallowAddress\":{\"city\":\"Warsaw\",\"zip\":\"00-001\"}}"); + // The top-level shallowAddress reference itself may be created (Jackson behavior) + // but its nested city/zip must remain null because depth-1 isn't allowed. + if (action.getShallowAddress() != null) { + assertNull("nested city should be rejected (depth insufficient)", + action.getShallowAddress().getCity()); + assertNull("nested zip should be rejected (depth insufficient)", + action.getShallowAddress().getZip()); + } + } +} diff --git a/plugins/rest/src/test/java/org/apache/struts2/rest/SecureRestAction.java b/plugins/rest/src/test/java/org/apache/struts2/rest/SecureRestAction.java new file mode 100644 index 000000000..16966dec1 --- /dev/null +++ b/plugins/rest/src/test/java/org/apache/struts2/rest/SecureRestAction.java @@ -0,0 +1,70 @@ +/* + * 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 + * + * 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.struts2.rest; + +import org.apache.struts2.ActionSupport; +import org.apache.struts2.interceptor.parameter.StrutsParameter; + +/** + * Test fixture for ContentTypeInterceptor integration tests. + * Has annotated and unannotated properties to exercise authorization filtering. + */ +public class SecureRestAction extends ActionSupport { + + private String name; + private String role; + private Address address; + private Address shallowAddress; + + public String getName() { return name; } + + @StrutsParameter + public void setName(String name) { this.name = name; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + + // Both annotations needed for REST: setter authorizes the top-level "address" (depth 0), + // getter(depth=1) authorizes nested "address.city" (depth 1). Note: ParametersInterceptor + // only requires the getter annotation — REST's recursive copy authorizes each path level + // independently. This divergence is tracked for the Approach C refactor. + @StrutsParameter(depth = 1) + public Address getAddress() { return address; } + + @StrutsParameter + public void setAddress(Address address) { this.address = address; } + + // shallowAddress: depth-0 authorized (setter annotated), but nested fields rejected + // because the getter has no depth>=1 annotation. + public Address getShallowAddress() { return shallowAddress; } + + @StrutsParameter + public void setShallowAddress(Address shallowAddress) { this.shallowAddress = shallowAddress; } + + public static class Address { + private String city; + private String zip; + + public String getCity() { return city; } + public void setCity(String city) { this.city = city; } + + public String getZip() { return zip; } + public void setZip(String zip) { this.zip = zip; } + } +}
