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 0997d17bfb9239113fcf95da4a57d2e1587ca7b2
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Nov 2 07:49:18 2026 +0100

    WW-4164 feat(core): reject disallowed extensions with 
ActionExtensionInterceptor
    
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
---
 .../struts2/config/impl/DefaultConfiguration.java  |  18 ++-
 .../org/apache/struts2/dispatcher/Dispatcher.java  |  17 ++-
 .../interceptor/ActionExtensionInterceptor.java    |  95 +++++++++++++++
 .../struts2/interceptor/ResultResolution.java      |  49 ++++++++
 .../httpmethod/HttpMethodInterceptor.java          |  16 +--
 core/src/main/resources/struts-default.xml         |   4 +
 .../XmlConfigurationProviderExtensionTest.java     |  14 +++
 .../apache/struts2/dispatcher/DispatcherTest.java  |  27 +++++
 .../ActionExtensionInterceptorTest.java            | 130 +++++++++++++++++++++
 9 files changed, 351 insertions(+), 19 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java 
b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
index 8ee6999ba..a66f31076 100644
--- 
a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
+++ 
b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
@@ -93,6 +93,7 @@ import org.apache.struts2.ognl.SecurityMemberAccess;
 import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
 import org.apache.struts2.ognl.accessor.RootAccessor;
 import org.apache.struts2.ognl.accessor.XWorkMethodAccessor;
+import org.apache.struts2.interceptor.ActionExtensionInterceptor;
 import org.apache.struts2.interceptor.httpmethod.HttpMethodInterceptor;
 import org.apache.struts2.interceptor.parameter.OgnlParameterAllowlister;
 import org.apache.struts2.interceptor.parameter.ParameterAllowlister;
@@ -562,6 +563,11 @@ public class DefaultConfiguration implements Configuration 
{
 
         warnIfHttpMethodNotEnforced(baseConfig, interceptors);
 
+        List<String> extensions = baseConfig.getExtensions() != null
+                ? baseConfig.getExtensions()
+                : packageContext.getFullExtensions();
+        warnIfExtensionNotEnforced(baseConfig, extensions, interceptors);
+
         String methodRegex = container.getInstance(String.class, 
StrutsConstants.STRUTS_SMI_METHOD_REGEX);
         if (methodRegex == null) {
             methodRegex = ActionConfig.DEFAULT_METHOD_REGEX;
@@ -569,10 +575,6 @@ public class DefaultConfiguration implements Configuration 
{
 
         LOG.debug("Using pattern [{}] to match allowed methods when SMI is 
disabled!", methodRegex);
 
-        List<String> extensions = baseConfig.getExtensions() != null
-                ? baseConfig.getExtensions()
-                : packageContext.getFullExtensions();
-
         return new ActionConfig.Builder(baseConfig)
                 .extensions(extensions)
                 .addParams(params)
@@ -593,6 +595,14 @@ public class DefaultConfiguration implements Configuration 
{
         }
     }
 
+    private void warnIfExtensionNotEnforced(ActionConfig baseConfig, 
List<String> extensions, List<InterceptorMapping> interceptors) {
+        if (extensions != null
+                && interceptors.stream().noneMatch(mapping -> 
mapping.getInterceptor() instanceof ActionExtensionInterceptor)) {
+            LOG.warn("Action [{}] in package [{}] declares extension {} but 
its interceptors do not include {}, the restriction will not be enforced",
+                    baseConfig.getName(), baseConfig.getPackageName(), 
extensions, ActionExtensionInterceptor.class.getSimpleName());
+        }
+    }
+
     private static class RuntimeConfigurationImpl implements 
RuntimeConfiguration {
 
         private final Map<String, Map<String, ActionConfig>> 
namespaceActionConfigs;
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java 
b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
index d249121f5..392bf252f 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
@@ -59,6 +59,7 @@ import org.apache.struts2.config.entities.PackageConfig;
 import org.apache.struts2.config.providers.XmlConfigurationProvider;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
 import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
 import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
 import org.apache.struts2.inject.Container;
@@ -732,7 +733,8 @@ public class Dispatcher {
             init_AliasStandardObjects(); // [7]
             init_DeferredXmlConfigurations();
 
-            getContainer(); // Inject this instance
+            Container container = getContainer(); // Inject this instance
+            warnIfExtensionsIgnoredByMapper(container);
 
             if (!dispatcherListeners.isEmpty()) {
                 for (DispatcherListener l : dispatcherListeners) {
@@ -750,6 +752,19 @@ public class Dispatcher {
         }
     }
 
+    private void warnIfExtensionsIgnoredByMapper(Container container) {
+        if (container.getInstance(ActionMapper.class).getClass() == 
DefaultActionMapper.class) {
+            return;
+        }
+        boolean declared = 
configurationManager.getConfiguration().getRuntimeConfiguration().getActionConfigs().values().stream()
+                .flatMap(actions -> actions.values().stream())
+                .anyMatch(config -> config.getExtensions() != null);
+        if (declared) {
+            LOG.warn("Actions declare the extension attribute but the 
configured {} is not {}, the declared extensions will not be applied",
+                    ActionMapper.class.getSimpleName(), 
DefaultActionMapper.class.getSimpleName());
+        }
+    }
+
     protected ConfigurationManager createConfigurationManager(String name) {
         return new ConfigurationManager(name);
     }
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/ActionExtensionInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/ActionExtensionInterceptor.java
new file mode 100644
index 000000000..464ecdc5e
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/ActionExtensionInterceptor.java
@@ -0,0 +1,95 @@
+/*
+ * 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.interceptor;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.ActionInvocation;
+import org.apache.struts2.ActionProxy;
+import org.apache.struts2.action.Action;
+import org.apache.struts2.dispatcher.mapper.ActionExtensions;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Rejects requests whose URL extension is not among the extensions declared 
on the action or its package
+ * with the {@code extension} attribute. Actions without a declaration are not 
restricted.
+ * <p>
+ * A rejected request resolves to {@link #setBadExtensionResultName(String) 
badExtensionResultName}
+ * when the action maps that result, otherwise the response is {@code 404 Not 
Found}.
+ * <p>
+ * Only the request's own action is checked; actions reached by chaining are 
not.
+ *
+ * @since 7.5.0
+ */
+public class ActionExtensionInterceptor extends AbstractInterceptor {
+
+    private static final Logger LOG = 
LogManager.getLogger(ActionExtensionInterceptor.class);
+
+    private String badExtensionResultName = "bad-extension";
+
+    @Override
+    public String intercept(ActionInvocation invocation) throws Exception {
+        ActionProxy proxy = invocation.getProxy();
+        List<String> allowed = proxy.getConfig().getExtensions();
+        if (allowed == null) {
+            return invocation.invoke();
+        }
+
+        ActionMapping mapping = 
invocation.getInvocationContext().getActionMapping();
+        if (mapping == null
+                || !Objects.equals(mapping.getName(), proxy.getActionName())
+                || !Objects.equals(mapping.getNamespace(), 
proxy.getNamespace())) {
+            return invocation.invoke();
+        }
+
+        if (ActionExtensions.allows(allowed, mapping.getExtension())) {
+            return invocation.invoke();
+        }
+
+        LOG.debug("Extension [{}] is not among the declared extensions {}, 
rejecting request!", mapping.getExtension(), allowed);
+        return rejectRequest(invocation);
+    }
+
+    /**
+     * @return the bad extension result name when the action maps that result, 
otherwise {@link Action#NONE}
+     * after sending {@code 404 Not Found}
+     * @since 7.5.0
+     */
+    protected String rejectRequest(ActionInvocation invocation) throws 
IOException {
+        if (ResultResolution.canResolve(invocation, badExtensionResultName)) {
+            return badExtensionResultName;
+        }
+
+        LOG.debug("No result mapped for: {}, sending: {}", 
badExtensionResultName, HttpServletResponse.SC_NOT_FOUND);
+        
invocation.getInvocationContext().getServletResponse().sendError(HttpServletResponse.SC_NOT_FOUND);
+        return Action.NONE;
+    }
+
+    /**
+     * @since 7.5.0
+     */
+    public void setBadExtensionResultName(String badExtensionResultName) {
+        this.badExtensionResultName = badExtensionResultName;
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/ResultResolution.java 
b/core/src/main/java/org/apache/struts2/interceptor/ResultResolution.java
new file mode 100644
index 000000000..6117785a7
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/interceptor/ResultResolution.java
@@ -0,0 +1,49 @@
+/*
+ * 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.interceptor;
+
+import org.apache.struts2.ActionInvocation;
+import org.apache.struts2.UnknownHandlerManager;
+import org.apache.struts2.config.entities.ActionConfig;
+
+/**
+ * Decides whether a rejection result name can be rendered for an invocation.
+ *
+ * @since 7.5.0
+ */
+public final class ResultResolution {
+
+    private ResultResolution() {
+    }
+
+    /**
+     * @return true when the action maps {@code resultName} (or a catch-all) 
or an unknown handler supplies a result
+     * @since 7.5.0
+     */
+    public static boolean canResolve(ActionInvocation invocation, String 
resultName) {
+        ActionConfig config = invocation.getProxy().getConfig();
+        if (config.getResults().containsKey(resultName) || 
config.getResults().containsKey("*")) {
+            return true;
+        }
+        UnknownHandlerManager unknownHandlerManager = 
invocation.getInvocationContext().getContainer().getInstance(UnknownHandlerManager.class);
+        return unknownHandlerManager.hasUnknownHandlers()
+            && 
unknownHandlerManager.handleUnknownResult(invocation.getInvocationContext(),
+                invocation.getProxy().getActionName(), config, resultName) != 
null;
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
index c6dbfc0d3..c60267838 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
@@ -19,10 +19,9 @@
 package org.apache.struts2.interceptor.httpmethod;
 
 import org.apache.struts2.ActionInvocation;
-import org.apache.struts2.UnknownHandlerManager;
 import org.apache.struts2.action.Action;
-import org.apache.struts2.config.entities.ActionConfig;
 import org.apache.struts2.interceptor.AbstractInterceptor;
+import org.apache.struts2.interceptor.ResultResolution;
 import org.apache.struts2.util.AnnotationUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -158,7 +157,7 @@ public class HttpMethodInterceptor extends 
AbstractInterceptor {
      */
     protected String rejectRequest(ActionInvocation invocation, 
Collection<HttpMethod> allowedMethods) throws IOException {
         String resultName = getBadRequestResultName(invocation);
-        if (canResolveResult(invocation, resultName)) {
+        if (ResultResolution.canResolve(invocation, resultName)) {
             return resultName;
         }
 
@@ -170,17 +169,6 @@ public class HttpMethodInterceptor extends 
AbstractInterceptor {
         return Action.NONE;
     }
 
-    private boolean canResolveResult(ActionInvocation invocation, String 
resultName) {
-        ActionConfig config = invocation.getProxy().getConfig();
-        if (config.getResults().containsKey(resultName) || 
config.getResults().containsKey("*")) {
-            return true;
-        }
-        UnknownHandlerManager unknownHandlerManager = 
invocation.getInvocationContext().getContainer().getInstance(UnknownHandlerManager.class);
-        return unknownHandlerManager.hasUnknownHandlers()
-            && 
unknownHandlerManager.handleUnknownResult(invocation.getInvocationContext(),
-                invocation.getProxy().getActionName(), config, resultName) != 
null;
-    }
-
     protected List<HttpMethod> readAllowedMethods(AnnotatedElement element) {
         List<HttpMethod> allowedMethods = new ArrayList<>();
         if (AnnotationUtils.isAnnotatedBy(element, AllowedHttpMethod.class)) {
diff --git a/core/src/main/resources/struts-default.xml 
b/core/src/main/resources/struts-default.xml
index f9a0eced0..610420492 100644
--- a/core/src/main/resources/struts-default.xml
+++ b/core/src/main/resources/struts-default.xml
@@ -86,6 +86,7 @@
             <interceptor name="noop" 
class="org.apache.struts2.interceptor.NoOpInterceptor"/>
             <interceptor name="fetchMetadata" 
class="org.apache.struts2.interceptor.FetchMetadataInterceptor"/>
             <interceptor name="httpMethod" 
class="org.apache.struts2.interceptor.httpmethod.HttpMethodInterceptor" />
+            <interceptor name="actionExtension" 
class="org.apache.struts2.interceptor.ActionExtensionInterceptor"/>
 
             <!-- Empty stack - performs no operations -->
             <interceptor-stack name="emptyStack">
@@ -97,6 +98,7 @@
                 <interceptor-ref name="exception"/>
                 <interceptor-ref name="servletConfig"/>
                 <interceptor-ref name="httpMethod"/>
+                <interceptor-ref name="actionExtension"/>
                 <interceptor-ref name="prepare"/>
                 <interceptor-ref name="checkbox"/>
                 <interceptor-ref name="datetime"/>
@@ -159,6 +161,7 @@
                 <interceptor-ref name="params"/>
                 <interceptor-ref name="servletConfig"/>
                 <interceptor-ref name="httpMethod"/>
+                <interceptor-ref name="actionExtension"/>
                 <interceptor-ref name="prepare"/>
                 <interceptor-ref name="chain"/>
                 <interceptor-ref name="modelDriven"/>
@@ -191,6 +194,7 @@
                 <interceptor-ref name="alias"/>
                 <interceptor-ref name="servletConfig"/>
                 <interceptor-ref name="httpMethod"/>
+                <interceptor-ref name="actionExtension"/>
                 <interceptor-ref name="i18n"/>
                 <interceptor-ref name="csp">
                     <param name="disabled">false</param>
diff --git 
a/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderExtensionTest.java
 
b/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderExtensionTest.java
index 4e318e57c..175c779a9 100644
--- 
a/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderExtensionTest.java
+++ 
b/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderExtensionTest.java
@@ -22,6 +22,9 @@ import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.config.ConfigurationException;
 import org.apache.struts2.config.StrutsXmlConfigurationProvider;
 import org.apache.struts2.config.entities.ActionConfig;
+import org.apache.struts2.config.impl.DefaultConfiguration;
+import org.apache.struts2.test.LogCapture;
+import org.apache.logging.log4j.Level;
 
 import java.util.List;
 
@@ -63,6 +66,17 @@ public class XmlConfigurationProviderExtensionTest extends 
StrutsInternalTestCas
         assertThat(actionConfig("/wild", 
"edit_user").getExtensions()).isEqualTo(List.of("html"));
     }
 
+    public void testWarnsWhenStackLacksActionExtensionInterceptor() {
+        try (LogCapture capture = new LogCapture(DefaultConfiguration.class)) {
+            loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(CONFIG));
+
+            assertThat(capture.messages(Level.WARN))
+                .anyMatch(message -> message.contains("Action [inherits] in 
package [child] declares extension [html]")
+                    && message.contains("ActionExtensionInterceptor"))
+                .noneMatch(message -> message.contains("Action [undeclared]"));
+        }
+    }
+
     public void testLeadingDotRejected() {
         StrutsXmlConfigurationProvider provider = new 
StrutsXmlConfigurationProvider(INVALID_CONFIG);
         assertThatThrownBy(() -> loadConfigurationProviders(provider))
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java 
b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java
index 970863769..a727586a4 100644
--- a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java
+++ b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java
@@ -42,6 +42,7 @@ import org.apache.struts2.test.StubConfigurationProvider;
 import org.apache.struts2.util.location.LocatableProperties;
 import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.test.LogCapture;
 import org.apache.struts2.StrutsJUnit4InternalTestCase;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
@@ -632,6 +633,32 @@ public class DispatcherTest extends 
StrutsJUnit4InternalTestCase {
                 Locale.getDefault(), dispatcher.getLocale(request));
     }
 
+    @Test
+    public void testWarnsWhenNonDefaultMapperIgnoresDeclaredExtensions() {
+        Map<String, String> params = new HashMap<>();
+        params.put("config", 
"struts-default.xml,struts-plugin.xml,org/apache/struts2/config/providers/struts-extension.xml");
+        params.put(StrutsConstants.STRUTS_MAPPER_CLASS, "restful");
+
+        try (LogCapture capture = new LogCapture(Dispatcher.class)) {
+            initDispatcher(params);
+
+            assertThat(capture.messages(Level.WARN)).singleElement().asString()
+                    .contains("declared extensions will not be applied");
+        }
+    }
+
+    @Test
+    public void testNoMapperWarningWithDefaultMapper() {
+        Map<String, String> params = new HashMap<>();
+        params.put("config", 
"struts-default.xml,struts-plugin.xml,org/apache/struts2/config/providers/struts-extension.xml");
+
+        try (LogCapture capture = new LogCapture(Dispatcher.class)) {
+            initDispatcher(params);
+
+            assertThat(capture.messages(Level.WARN)).noneMatch(message -> 
message.contains("declared extensions"));
+        }
+    }
+
     @Test
     public void 
testGetLocale_With_BadDefaultLocale_WarnsWithResolvedLocaleNotRequestLocale() {
         Map<String, String> params = new HashMap<>();
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/ActionExtensionInterceptorTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/ActionExtensionInterceptorTest.java
new file mode 100644
index 000000000..0510e23e8
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/ActionExtensionInterceptorTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.interceptor;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.TestAction;
+import org.apache.struts2.action.Action;
+import org.apache.struts2.config.entities.ActionConfig;
+import org.apache.struts2.config.entities.ResultConfig;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.mock.MockActionInvocation;
+import org.apache.struts2.mock.MockActionProxy;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ActionExtensionInterceptorTest extends StrutsInternalTestCase {
+
+    private ActionExtensionInterceptor interceptor;
+    private MockActionInvocation invocation;
+    private MockHttpServletResponse response;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        interceptor = new ActionExtensionInterceptor();
+    }
+
+    public void testUndeclaredPasses() throws Exception {
+        prepare(config(null), "second", "second", "html");
+
+        assertThat(interceptor.intercept(invocation)).isEqualTo("success");
+    }
+
+    public void testAllowedPasses() throws Exception {
+        prepare(config(List.of("html")), "second", "second", "html");
+
+        assertThat(interceptor.intercept(invocation)).isEqualTo("success");
+    }
+
+    public void testNullMappingExtensionCountsAsEmpty() throws Exception {
+        prepare(config(List.of("action", "")), "second", "second", null);
+
+        assertThat(interceptor.intercept(invocation)).isEqualTo("success");
+    }
+
+    public void testRejectedWithoutResultSends404() throws Exception {
+        prepare(config(List.of("html")), "second", "second", "action");
+
+        assertThat(interceptor.intercept(invocation)).isEqualTo(Action.NONE);
+        
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND);
+    }
+
+    public void testRejectedUsesBadExtensionResult() throws Exception {
+        prepare(config(List.of("html"), "bad-extension"), "second", "second", 
"action");
+
+        
assertThat(interceptor.intercept(invocation)).isEqualTo("bad-extension");
+        assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
+    }
+
+    public void testCustomResultName() throws Exception {
+        interceptor.setBadExtensionResultName("wrong-ext");
+        prepare(config(List.of("html"), "wrong-ext"), "second", "second", 
"action");
+
+        assertThat(interceptor.intercept(invocation)).isEqualTo("wrong-ext");
+    }
+
+    public void testChainedInvocationNotEnforced() throws Exception {
+        prepare(config(List.of("html")), "first", "second", "action");
+
+        assertThat(interceptor.intercept(invocation)).isEqualTo("success");
+    }
+
+    public void testNoMappingPasses() throws Exception {
+        prepare(config(List.of("html")), null, "second", null);
+
+        assertThat(interceptor.intercept(invocation)).isEqualTo("success");
+    }
+
+    private ActionConfig config(List<String> extensions, String... 
resultNames) {
+        ActionConfig.Builder builder = new ActionConfig.Builder("pkg", 
"second", TestAction.class.getName())
+            .extensions(extensions);
+        for (String resultName : resultNames) {
+            builder.addResultConfig(new ResultConfig.Builder(resultName, 
"result").build());
+        }
+        return builder.build();
+    }
+
+    private void prepare(ActionConfig config, String mappingName, String 
proxyName, String extension) {
+        MockActionProxy proxy = new MockActionProxy();
+        proxy.setConfig(config);
+        proxy.setActionName(proxyName);
+        proxy.setNamespace("/");
+        invocation = new MockActionInvocation();
+        invocation.setProxy(proxy);
+        invocation.setAction(new TestAction());
+        invocation.setResultCode("success");
+        response = new MockHttpServletResponse();
+        ActionContext context = ActionContext.getContext()
+            .withServletRequest(new MockHttpServletRequest("GET", "/x"))
+            .withServletResponse(response);
+        if (mappingName != null) {
+            ActionMapping mapping = new ActionMapping(mappingName, "/", "", 
null);
+            mapping.setExtension(extension);
+            context.withActionMapping(mapping);
+        }
+        invocation.setInvocationContext(context);
+    }
+}

Reply via email to