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

lukaszlenart pushed a commit to branch WW-5626-approach-c
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 04f78e287acd72e318f1dc53b8cd90a7cba0e6c8
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon May 4 16:05:25 2026 +0200

    WW-5626 add integration tests proving the new Jackson authorization path is 
used
---
 .../ContentTypeInterceptorIntegrationTest.java     | 91 +++++++++++++++++++---
 1 file changed, 81 insertions(+), 10 deletions(-)

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
index dc11c6fdf..dc4812c2a 100644
--- 
a/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java
+++ 
b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java
@@ -23,8 +23,10 @@ import com.mockobjects.dynamic.Mock;
 import junit.framework.TestCase;
 import org.apache.struts2.ActionContext;
 import org.apache.struts2.ActionInvocation;
+import org.apache.struts2.ActionSupport;
 import org.apache.struts2.action.Action;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.interceptor.parameter.StrutsParameter;
 import org.apache.struts2.interceptor.parameter.StrutsParameterAuthorizer;
 import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory;
 import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory;
@@ -53,7 +55,10 @@ public class ContentTypeInterceptorIntegrationTest extends 
TestCase {
     protected void setUp() throws Exception {
         super.setUp();
         action = new SecureRestAction();
+        setupInterceptorWithAction(action);
+    }
 
+    private void setupInterceptorWithAction(Object actionInstance) {
         var ognlUtil = new OgnlUtil(
                 new DefaultOgnlExpressionCacheFactory<>("1000", 
LRU.toString()),
                 new DefaultOgnlBeanInfoCacheFactory<>("1000", LRU.toString()),
@@ -72,8 +77,8 @@ public class ContentTypeInterceptorIntegrationTest extends 
TestCase {
         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("getAction", actionInstance);
+        mockActionInvocation.expectAndReturn("getAction", actionInstance);
         mockActionInvocation.expectAndReturn("invoke", Action.SUCCESS);
         mockSelector.expectAndReturn("getHandlerForRequest", new 
AnyConstraintMatcher() {
             public boolean matches(Object[] args) { return true; }
@@ -120,15 +125,81 @@ public class ContentTypeInterceptorIntegrationTest 
extends TestCase {
     }
 
     public void testNestedPropertyRejectedWhenDepthInsufficient() throws 
Exception {
-        // shallowAddress has @StrutsParameter (depth=0) — its nested fields 
should NOT be set
+        // shallowAddress has @StrutsParameter on the setter (depth-0 
authorized) but the getter
+        // has no depth>=1 annotation. The Jackson path enters shallowAddress 
(constructed by
+        // Jackson) but skipChildren on each inner property — so the Address 
is non-null but its
+        // city/zip fields stay null.
         
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());
+        assertNotNull("shallowAddress is depth-0 authorized; Jackson 
constructs it",
+                action.getShallowAddress());
+        assertNull("nested city must be rejected (depth-1 not authorized)",
+                action.getShallowAddress().getCity());
+        assertNull("nested zip must be rejected (depth-1 not authorized)",
+                action.getShallowAddress().getZip());
+    }
+
+    // --- Tests proving the new Jackson authorization path is in use ---
+
+    public void testJacksonHandlerDoesNotRequireNoArgConstructor() throws 
Exception {
+        // The legacy two-phase copy required a no-arg constructor on the 
target. Jackson's
+        // readerForUpdating populates the existing instance directly, so this 
constraint
+        // is gone — proof that the new AuthorizationAware path is being taken.
+        NoNoArgAction noNoArg = new 
NoNoArgAction("preserved-pre-deserialization-value");
+        setupInterceptorWithAction(noNoArg);
+        runWithBody("{\"name\":\"alice\"}");
+        assertEquals("alice", noNoArg.getName());
+        assertEquals("pre-existing field must be preserved (no fresh-instance 
copy)",
+                "preserved-pre-deserialization-value", 
noNoArg.getRequiredField());
+    }
+
+    public void testRejectedAtParentNeverInstantiatesNestedObject() throws 
Exception {
+        // Stronger guarantee than the two-phase copy: when the parent 
property is rejected,
+        // Jackson's skipChildren() discards the entire JSON subtree and the 
nested object
+        // is never constructed. role-typed fixture: address requires a setter 
@StrutsParameter
+        // for depth-0 authorization. By giving address a fresh action where 
address is depth-0
+        // unauthorized, we prove the setter is never called and address stays 
null.
+        // (We use a custom action where address has no setter annotation.)
+        UnauthorizedNestedAction restrictedAction = new 
UnauthorizedNestedAction();
+        setupInterceptorWithAction(restrictedAction);
+        runWithBody("{\"unauthorized\":{\"city\":\"Warsaw\"}}");
+        assertNull("unauthorized property must be rejected at parent — Jackson 
never enters",
+                restrictedAction.getUnauthorized());
+    }
+
+    // --- Test fixtures for new path verification ---
+
+    /**
+     * Action with no public no-arg constructor — would fail the legacy 
two-phase copy's
+     * createFreshInstance check, but works fine with the Jackson 
authorization path.
+     */
+    public static class NoNoArgAction extends ActionSupport {
+        private final String requiredField;
+        private String name;
+
+        public NoNoArgAction(String requiredField) {
+            this.requiredField = requiredField;
+        }
+
+        public String getName() { return name; }
+
+        @StrutsParameter
+        public void setName(String name) { this.name = name; }
+
+        public String getRequiredField() { return requiredField; }
+    }
+
+    /**
+     * Action with a property that has NO @StrutsParameter on its setter — 
depth-0 authorization
+     * fails, so Jackson must never enter this property nor instantiate the 
nested object.
+     */
+    public static class UnauthorizedNestedAction extends ActionSupport {
+        private SecureRestAction.Address unauthorized;
+
+        public SecureRestAction.Address getUnauthorized() { return 
unauthorized; }
+
+        // No @StrutsParameter annotation — depth-0 path "unauthorized" is 
rejected.
+        public void setUnauthorized(SecureRestAction.Address unauthorized) {
+            this.unauthorized = unauthorized;
         }
     }
 }

Reply via email to