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; }
+    }
+}

Reply via email to