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