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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git

commit e824a9c176cf6e81d98203d5a1e25ac289ea1247
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Nov 2 08:19:16 2026 +0100

    WW-4164 test(core): pin extension behaviour with one declaring package
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../dispatcher/ActionExtensionEndToEndTest.java    | 159 +++++++++++++++++++++
 .../dispatcher/struts-extension-one-package.xml    |  33 +++++
 2 files changed, 192 insertions(+)

diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java
new file mode 100644
index 000000000..6039d2222
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.dispatcher;
+
+import jakarta.servlet.ServletException;
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.action.Action;
+import org.apache.struts2.components.URL;
+import org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.util.ValueStack;
+import org.apache.struts2.util.ValueStackFactory;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockFilterConfig;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ActionExtensionEndToEndTest extends StrutsInternalTestCase {
+
+    private static final String EXECUTED = 
"actionExtensionEndToEndTest.executed";
+
+    private StrutsPrepareAndExecuteFilter filter;
+    private MockHttpServletRequest request;
+    private MockHttpServletResponse response;
+    private MockFilterChain filterChain;
+
+    public static class RecordingAction implements Action {
+        @Override
+        public String execute() {
+            ServletActionContext.getRequest().setAttribute(EXECUTED, 
Boolean.TRUE);
+            return NONE;
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        PrepareOperations.clearDevModeOverride();
+        Map<String, String> params = new HashMap<>();
+        params.put("config", 
"struts-default.xml,org/apache/struts2/dispatcher/struts-extension-one-package.xml");
+        params.put(StrutsConstants.STRUTS_ACTION_EXTENSION, "action,,");
+        initDispatcher(params);
+
+        filter = new StrutsPrepareAndExecuteFilter() {
+            @Override
+            protected InitOperations createInitOperations() {
+                return new InitOperations() {
+                    @Override
+                    public Dispatcher initDispatcher(HostConfig filterConfig) {
+                        return dispatcher;
+                    }
+                };
+            }
+        };
+        filter.init(new MockFilterConfig(servletContext));
+    }
+
+    public void testStaticHtmlFallsThrough() throws Exception {
+        doFilter("/index.html");
+
+        assertThat(filterChain.getRequest()).isNotNull();
+        assertThat(request.getAttribute(EXECUTED)).isNull();
+    }
+
+    public void testDeclaredActionServedOnItsExtension() throws Exception {
+        doFilter("/shop/cart.html");
+
+        assertThat(filterChain.getRequest()).isNull();
+        assertThat(request.getAttribute(EXECUTED)).isEqualTo(Boolean.TRUE);
+        assertThat(response.getStatus()).isEqualTo(200);
+    }
+
+    public void testDeclaredActionRejectedOnGlobalExtension() throws Exception 
{
+        doFilter("/shop/cart.action");
+
+        assertThat(response.getStatus()).isEqualTo(404);
+        assertThat(request.getAttribute(EXECUTED)).isNull();
+        assertThat(filterChain.getRequest()).isNull();
+    }
+
+    public void testUndeclaredActionUnchanged() throws Exception {
+        doFilter("/home.action");
+
+        assertThat(filterChain.getRequest()).isNull();
+        assertThat(request.getAttribute(EXECUTED)).isEqualTo(Boolean.TRUE);
+        assertThat(response.getStatus()).isEqualTo(200);
+
+        doFilter("/home");
+
+        assertThat(filterChain.getRequest()).isNull();
+        assertThat(request.getAttribute(EXECUTED)).isEqualTo(Boolean.TRUE);
+        assertThat(response.getStatus()).isEqualTo(200);
+    }
+
+    public void testUndeclaredActionNotServedOnDeclaredOnlyExtension() throws 
Exception {
+        doFilter("/home.html");
+
+        assertThat(filterChain.getRequest()).isNotNull();
+        assertThat(request.getAttribute(EXECUTED)).isNull();
+    }
+
+    public void testLinkFromHtmlPageToUndeclaredAction() {
+        MockHttpServletRequest pageRequest = new MockHttpServletRequest();
+        MockHttpServletResponse pageResponse = new MockHttpServletResponse();
+        ActionMapping current = new ActionMapping("cart", "/shop", null, null);
+        current.setExtension("html");
+        ValueStack stack = 
container.getInstance(ValueStackFactory.class).createValueStack();
+        ActionContext context = stack.getActionContext()
+                .withContainer(container)
+                .withValueStack(stack)
+                .withActionMapping(current)
+                .withServletRequest(pageRequest)
+                .withServletResponse(pageResponse)
+                .bind();
+
+        URL url = new URL(context.getValueStack(), pageRequest, pageResponse);
+        container.inject(url);
+        url.setAction("home");
+        url.setNamespace("/");
+        StringWriter writer = new StringWriter();
+        url.start(writer);
+        url.end(writer, "");
+
+        assertThat(writer.toString()).endsWith("/home.action");
+    }
+
+    private void doFilter(String uri) throws ServletException, IOException {
+        request = new MockHttpServletRequest(servletContext);
+        request.setRequestURI(uri);
+        response = new MockHttpServletResponse();
+        filterChain = new MockFilterChain();
+        filter.doFilter(request, response, filterChain);
+    }
+}
diff --git 
a/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
 
b/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
new file mode 100644
index 000000000..54346be82
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+    "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN"
+    "https://struts.apache.org/dtds/struts-7.5.dtd";>
+<struts>
+    <package name="shop" extends="struts-default" namespace="/shop" 
extension="html">
+        <action name="cart" 
class="org.apache.struts2.dispatcher.ActionExtensionEndToEndTest$RecordingAction"/>
+    </package>
+
+    <package name="root" extends="struts-default" namespace="/">
+        <action name="home" 
class="org.apache.struts2.dispatcher.ActionExtensionEndToEndTest$RecordingAction"/>
+    </package>
+</struts>

Reply via email to