diff --git a/api/src/org/labkey/api/action/SpringActionController.java b/api/src/org/labkey/api/action/SpringActionController.java index 359eec144bc..a66b95382df 100644 --- a/api/src/org/labkey/api/action/SpringActionController.java +++ b/api/src/org/labkey/api/action/SpringActionController.java @@ -1257,8 +1257,6 @@ public static void checkForMutatingSql(Supplier mutatingSqlSupplier) Class actionClass = getActionForThread(); if (null == actionClass) return; - if (actionClass.getName().contains("JunitController")) - return; ViewContext vc = HttpView.currentContext(); boolean readonly = false; diff --git a/api/src/org/labkey/api/view/ViewContext.java b/api/src/org/labkey/api/view/ViewContext.java index 245ad95b7c5..7e213319ab7 100644 --- a/api/src/org/labkey/api/view/ViewContext.java +++ b/api/src/org/labkey/api/view/ViewContext.java @@ -164,6 +164,18 @@ public static StackResetter pushMockViewContext(User user, Container c, ActionUR return new StackResetter(context, stackSize); } + /** + * Ensures a view context is available without disturbing one that's already there: if a view is + * already on the stack, returns it wrapped in a no-op resetter; otherwise pushes a new mock context + * via {@link #pushMockViewContext}. + */ + public static StackResetter ensureViewContext(User user, Container c, ActionURL url) + { + if (HttpView.hasCurrentView()) + return new StackResetter(HttpView.currentContext(), HttpView.getStackSize()); + else + return pushMockViewContext(user, c, url); + } // Needed by background threads that call entrypoints that require ViewContexts // TODO: Well-behaved interfaces should not take ViewContexts -- clean up query, et al to remove ViewContext params @@ -178,7 +190,11 @@ public static ViewContext getMockViewContext(User user, Container c, ActionURL u if (null != url) context.setBindPropertyValues(url.getPropertyValues()); - HttpServletRequest request = ViewServlet.mockRequest("GET", url, user, null, null); + // Inherit current request's method, if present. + HttpServletRequest currentRequest = HttpView.currentRequest(); + String mockRequestMethod = pushViewContext && currentRequest != null ? currentRequest.getMethod() : "GET"; + + HttpServletRequest request = ViewServlet.mockRequest(mockRequestMethod, url, user, null, null); context.setRequest(request); // Major hack -- QueryView needs the context pushed onto the ViewContext stack in thread local diff --git a/core/src/org/labkey/core/junit/JunitController.java b/core/src/org/labkey/core/junit/JunitController.java index 96a7c0b88dc..b992f303d7e 100644 --- a/core/src/org/labkey/core/junit/JunitController.java +++ b/core/src/org/labkey/core/junit/JunitController.java @@ -32,7 +32,9 @@ import org.junit.runner.notification.Failure; import org.labkey.api.action.ApiResponse; import org.labkey.api.action.ApiSimpleResponse; +import org.labkey.api.action.BaseViewAction; import org.labkey.api.action.MutatingApiAction; +import org.labkey.api.action.NavTrailAction; import org.labkey.api.action.PermissionCheckableAction; import org.labkey.api.action.ReadOnlyApiAction; import org.labkey.api.action.SimpleViewAction; @@ -41,6 +43,7 @@ import org.labkey.api.action.StatusReportingRunnable; import org.labkey.api.action.StatusReportingRunnableAction; import org.labkey.api.jsp.JspTest; +import org.labkey.api.security.MethodsAllowed; import org.labkey.api.security.RequiresNoPermission; import org.labkey.api.security.RequiresSiteAdmin; import org.labkey.api.security.User; @@ -56,8 +59,11 @@ import org.labkey.api.view.NotFoundException; import org.labkey.api.view.template.PageConfig; import org.springframework.validation.BindException; +import org.springframework.validation.Errors; import org.springframework.web.servlet.ModelAndView; +import static org.labkey.api.util.HttpUtil.Method.POST; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; @@ -144,8 +150,38 @@ static public TestWhen.When getScope(Class cls) } + /** + * Like {@link SimpleViewAction} — one code path, no form redisplay — but POST-only and not one of + * the types {@link SpringActionController#checkForMutatingSql} treats as unconditionally read-only, + * so subclasses can legitimately run mutating SQL. + */ + @MethodsAllowed(POST) + private abstract static class SimplePostViewAction
extends BaseViewAction implements NavTrailAction + { + @Override + public final ModelAndView handleRequest() throws Exception + { + BindException errors = defaultBindParameters(getPropertyValues()); + return getView((FORM) errors.getTarget(), errors); + } + + @Override + protected final String getCommandClassMethodName() + { + return "getView"; + } + + @Override + public final void validate(Object target, Errors errors) + { + } + + public abstract ModelAndView getView(FORM form, BindException errors) throws Exception; + } + + @RequiresSiteAdmin - public class RunAction extends SimpleViewAction + public class RunAction extends SimplePostViewAction { @Override public ModelAndView getView(TestForm form, BindException errors) throws Exception @@ -220,7 +256,7 @@ public void addNavTrail(NavTree root) private static final String RESULTS_SESSION_KEY = "JUnit_Results"; @RequiresSiteAdmin - public static class Run3Action extends SimpleViewAction + public static class Run3Action extends SimplePostViewAction { @Override public ModelAndView getView(TestForm form, BindException errors) throws Exception @@ -300,6 +336,7 @@ public void addNavTrail(NavTree root) @RequiresSiteAdmin + @MethodsAllowed(POST) public static class Run2Action extends StatusReportingRunnableAction { private List> getTestClasses(TestForm form) diff --git a/core/src/org/labkey/core/junit/runner.jsp b/core/src/org/labkey/core/junit/runner.jsp index 5e838576973..0a8dceca8cc 100644 --- a/core/src/org/labkey/core/junit/runner.jsp +++ b/core/src/org/labkey/core/junit/runner.jsp @@ -26,10 +26,26 @@ <%@ page import="org.labkey.core.junit.JunitController.Run2Action" %> <%@ page import="org.labkey.core.junit.JunitController.Run3Action" %> <%@ page import="org.labkey.core.junit.JunitController.RunAction" %> +<%@ page import="java.util.stream.Stream" %> <%@ page import="static org.labkey.api.util.DOM.*" %> <%@ page import="static org.labkey.api.util.DOM.Attribute.*" %> <%@ page import="static org.labkey.api.util.HtmlString.NBSP" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + // Parameterized (and other Suite-based) runners nest a level of per-invocation Descriptions between the + // class and its test methods; flatten to leaves so every test method renders at the same list level. + private Stream leafDescriptions(Description desc) + { + return desc.isTest() ? Stream.of(desc) : desc.getChildren().stream().flatMap(this::leafDescriptions); + } + + private Renderable renderLeaf(Description desc, ActionURL testCaseURL) + { + return LI(desc.getMethodName() != null + ? simpleLink(desc.getMethodName(), testCaseURL.clone().addParameter("methodName", desc.getMethodName())).usePost() + : desc.toString()); + } +%> <% JspView me = HttpView.currentView(); JUnitViewBean bean = me.getModelBean(); @@ -76,18 +92,18 @@ NBSP, "\u22EE", NBSP, - button("Run All").href(new ActionURL(RunAction.class, getContainer())), + button("Run All").href(new ActionURL(RunAction.class, getContainer())).usePost(), NBSP, - button("Run BVT").href(new ActionURL(RunAction.class, getContainer()).addParameter("when", "BVT")), + button("Run BVT").href(new ActionURL(RunAction.class, getContainer()).addParameter("when", "BVT")).usePost(), NBSP, - button("Run DRT").href(new ActionURL(RunAction.class, getContainer()).addParameter("when", "DRT")), + button("Run DRT").href(new ActionURL(RunAction.class, getContainer()).addParameter("when", "DRT")).usePost(), NBSP, "\u22EE", NBSP, LK.FORM(at(style, "display:inline-block;", name, "run2", action, new ActionURL(Run2Action.class, getContainer()), method, "POST"), button("Run In Background #1 (Experimental)").submit(true)), NBSP, - button("Run In Background #2 (Experimental)").href(new ActionURL(Run3Action.class, getContainer())) + button("Run In Background #2 (Experimental)").href(new ActionURL(Run3Action.class, getContainer())).usePost() ), HR()).appendTo(out); @@ -95,7 +111,7 @@ DIV(testCases.keySet().stream().map(module -> DETAILS(at(open, true), - SUMMARY(A(at(href, new ActionURL(RunAction.class, getContainer()).addParameter("module", module)), module)), + SUMMARY(simpleLink(module, new ActionURL(RunAction.class, getContainer()).addParameter("module", module)).usePost()), DIV(cl("module-details"), testCases.get(module).stream().map(clazz -> { Runner runner = Request.aClass(clazz).getRunner(); Description desc = runner.getDescription(); @@ -105,13 +121,10 @@ return DIV(cl("labkey-indented"), DETAILS( SUMMARY( - A(at(href, testCaseURL.getLocalURIString()), displayName), + simpleLink(displayName, testCaseURL).usePost(), showRunButtons ? SPAN(cl("scope-tag", JunitController.getScope(clazz).name()), JunitController.getScope(clazz).name()) : null, (desc.testCount() > 1 ? SPAN(cl("test-count"), "(" + desc.testCount() + ")") : "")), - UL(desc.getChildren().stream().map( - child -> LI(child.getMethodName() != null - ? A(at(href, testCaseURL.clone().addParameter("methodName", child.getMethodName())), child.getMethodName()) - : child.toString()))) + UL(leafDescriptions(desc).map(child -> renderLeaf(child, testCaseURL))) )); })) diff --git a/study/src/org/labkey/study/assay/StudyPublishManager.java b/study/src/org/labkey/study/assay/StudyPublishManager.java index ffa977601c0..6a763de3e22 100644 --- a/study/src/org/labkey/study/assay/StudyPublishManager.java +++ b/study/src/org/labkey/study/assay/StudyPublishManager.java @@ -1118,7 +1118,7 @@ public void autoLinkDerivedSamples(ExpSampleType sampleType, List keys, Co if (sampleType != null && sampleType.getAutoLinkTargetContainer() != null) { // Issue 51454 : QueryView needs a view context to initialize properly. Ensure a mock view context when running in the background - try (EnsureViewContext ignore = new EnsureViewContext(container, user)) + try (ViewContext.StackResetter _ = ViewContext.ensureViewContext(user, container, new ActionURL())) { // attempt to auto link the results QuerySettings qs = new QuerySettings(new MutablePropertyValues(), QueryView.DATAREGIONNAME_DEFAULT); @@ -1208,7 +1208,7 @@ public void autoLinkSamples(ExpSampleType sampleType, List if (validStudies.contains(study)) { // Issue 49253 : QueryView needs a view context to initialize properly. Ensure a mock view context when running in the background - try (EnsureViewContext ignore = new EnsureViewContext(container, user)) + try (ViewContext.StackResetter _ = ViewContext.ensureViewContext(user, container, new ActionURL())) { LOG.debug("Resolved target study in container {} for auto-linking with {} from container {}", targetContainerPath, sampleTypeName, containerPath); List> dataMaps = new ArrayList<>(); @@ -1751,25 +1751,4 @@ private void getViewColumns(UserSchema userSchema, QuerySettings qs, Map