From 3378b5641736be88807cb4bf8885a1efef194d1e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 10 Sep 2026 14:21:13 -0700 Subject: [PATCH 1/7] Fix JUnit runner page's handling of parameterized tests --- core/src/org/labkey/core/junit/runner.jsp | 42 +++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/core/src/org/labkey/core/junit/runner.jsp b/core/src/org/labkey/core/junit/runner.jsp index 5e838576973..c13d42f8d76 100644 --- a/core/src/org/labkey/core/junit/runner.jsp +++ b/core/src/org/labkey/core/junit/runner.jsp @@ -26,10 +26,43 @@ <%@ 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="static org.labkey.api.util.DOM.*" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.*" %> +<%@ page import="java.util.stream.Stream" %> +<%@ page import="static DOM.A" %> +<%@ page import="static DOM.DETAILS" %> +<%@ page import="static DOM.DIV" %> +<%@ page import="static DOM.HR" %> +<%@ page import="static DOM.LI" %> +<%@ page import="static DOM.LK" %> +<%@ page import="static DOM.Renderable" %> +<%@ page import="static DOM.SPAN" %> +<%@ page import="static DOM.SUMMARY" %> +<%@ page import="static DOM.UL" %> +<%@ page import="static DOM.at" %> +<%@ page import="static DOM.cl" %> +<%@ page import="static DOM.createHtmlFragment" %> +<%@ page import="static org.labkey.api.util.DOM.Attribute.action" %> +<%@ page import="static org.labkey.api.util.DOM.Attribute.href" %> +<%@ page import="static org.labkey.api.util.DOM.Attribute.method" %> +<%@ page import="static org.labkey.api.util.DOM.Attribute.name" %> +<%@ page import="static org.labkey.api.util.DOM.Attribute.open" %> +<%@ page import="static org.labkey.api.util.DOM.Attribute.style" %> <%@ 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 + ? A(at(href, testCaseURL.clone().addParameter("methodName", desc.getMethodName())), desc.getMethodName()) + : desc.toString()); + } +%> <% JspView me = HttpView.currentView(); JUnitViewBean bean = me.getModelBean(); @@ -108,10 +141,7 @@ A(at(href, testCaseURL.getLocalURIString()), displayName), 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))) )); })) From e391ff98a2fb68e69669c670bc17bc8704627dbd Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 10 Sep 2026 14:43:42 -0700 Subject: [PATCH 2/7] Fix imports --- core/src/org/labkey/core/junit/runner.jsp | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/core/src/org/labkey/core/junit/runner.jsp b/core/src/org/labkey/core/junit/runner.jsp index c13d42f8d76..974d080ffd6 100644 --- a/core/src/org/labkey/core/junit/runner.jsp +++ b/core/src/org/labkey/core/junit/runner.jsp @@ -27,25 +27,8 @@ <%@ 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 DOM.A" %> -<%@ page import="static DOM.DETAILS" %> -<%@ page import="static DOM.DIV" %> -<%@ page import="static DOM.HR" %> -<%@ page import="static DOM.LI" %> -<%@ page import="static DOM.LK" %> -<%@ page import="static DOM.Renderable" %> -<%@ page import="static DOM.SPAN" %> -<%@ page import="static DOM.SUMMARY" %> -<%@ page import="static DOM.UL" %> -<%@ page import="static DOM.at" %> -<%@ page import="static DOM.cl" %> -<%@ page import="static DOM.createHtmlFragment" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.action" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.href" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.method" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.name" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.open" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.style" %> +<%@ 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" %> <%! From 32578cd84a818646207d8f5c343250f8ae933f30 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 10 Sep 2026 15:13:43 -0700 Subject: [PATCH 3/7] Require POST for JUnit runner actions that execute tests RunAction, Run2Action, and Run3Action accepted GET despite running tests, which can be expensive or side-effecting; add @MethodsAllowed(POST) and switch runner.jsp's buttons/links to POST accordingly. GoAction already required POST via MutatingApiAction. --- .../src/org/labkey/core/junit/JunitController.java | 6 ++++++ core/src/org/labkey/core/junit/runner.jsp | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/src/org/labkey/core/junit/JunitController.java b/core/src/org/labkey/core/junit/JunitController.java index 96a7c0b88dc..f9dcdfb105d 100644 --- a/core/src/org/labkey/core/junit/JunitController.java +++ b/core/src/org/labkey/core/junit/JunitController.java @@ -41,6 +41,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; @@ -58,6 +59,8 @@ import org.springframework.validation.BindException; 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; @@ -145,6 +148,7 @@ static public TestWhen.When getScope(Class cls) @RequiresSiteAdmin + @MethodsAllowed(POST) public class RunAction extends SimpleViewAction { @Override @@ -220,6 +224,7 @@ public void addNavTrail(NavTree root) private static final String RESULTS_SESSION_KEY = "JUnit_Results"; @RequiresSiteAdmin + @MethodsAllowed(POST) public static class Run3Action extends SimpleViewAction { @Override @@ -300,6 +305,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 974d080ffd6..0a8dceca8cc 100644 --- a/core/src/org/labkey/core/junit/runner.jsp +++ b/core/src/org/labkey/core/junit/runner.jsp @@ -42,7 +42,7 @@ private Renderable renderLeaf(Description desc, ActionURL testCaseURL) { return LI(desc.getMethodName() != null - ? A(at(href, testCaseURL.clone().addParameter("methodName", desc.getMethodName())), desc.getMethodName()) + ? simpleLink(desc.getMethodName(), testCaseURL.clone().addParameter("methodName", desc.getMethodName())).usePost() : desc.toString()); } %> @@ -92,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); @@ -111,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(); @@ -121,7 +121,7 @@ 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(leafDescriptions(desc).map(child -> renderLeaf(child, testCaseURL))) From bf4dcc482fe9f82dfdd27d9ba7df5dfb13c00daf Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 10 Sep 2026 16:17:45 -0700 Subject: [PATCH 4/7] Remove JunitController exclusion for mutating sql detection --- api/src/org/labkey/api/action/SpringActionController.java | 2 -- 1 file changed, 2 deletions(-) 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; From f6cedf8a0a683d23c709e4af750d35d4dc7bf4d6 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 11 Sep 2026 08:28:11 -0700 Subject: [PATCH 5/7] Fix actions --- .../labkey/core/junit/JunitController.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/core/src/org/labkey/core/junit/JunitController.java b/core/src/org/labkey/core/junit/JunitController.java index f9dcdfb105d..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; @@ -57,6 +59,7 @@ 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; @@ -147,9 +150,38 @@ static public TestWhen.When getScope(Class cls) } - @RequiresSiteAdmin + /** + * 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) - public class RunAction extends SimpleViewAction + 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 SimplePostViewAction { @Override public ModelAndView getView(TestForm form, BindException errors) throws Exception @@ -224,8 +256,7 @@ public void addNavTrail(NavTree root) private static final String RESULTS_SESSION_KEY = "JUnit_Results"; @RequiresSiteAdmin - @MethodsAllowed(POST) - public static class Run3Action extends SimpleViewAction + public static class Run3Action extends SimplePostViewAction { @Override public ModelAndView getView(TestForm form, BindException errors) throws Exception From dd068253069649dc4e9f12b3fea41e8bb3f08dde Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 11 Sep 2026 11:15:42 -0700 Subject: [PATCH 6/7] Make mock viewContexts inherit request type --- api/src/org/labkey/api/view/ViewContext.java | 13 +++++++++- .../study/assay/StudyPublishManager.java | 25 ++----------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/api/src/org/labkey/api/view/ViewContext.java b/api/src/org/labkey/api/view/ViewContext.java index 245ad95b7c5..7610b05df0d 100644 --- a/api/src/org/labkey/api/view/ViewContext.java +++ b/api/src/org/labkey/api/view/ViewContext.java @@ -164,6 +164,13 @@ public static StackResetter pushMockViewContext(User user, Container c, ActionUR return new StackResetter(context, stackSize); } + 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 +185,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/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 Date: Fri, 11 Sep 2026 14:54:54 -0700 Subject: [PATCH 7/7] Fix logic and add javadoc --- api/src/org/labkey/api/view/ViewContext.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/view/ViewContext.java b/api/src/org/labkey/api/view/ViewContext.java index 7610b05df0d..7e213319ab7 100644 --- a/api/src/org/labkey/api/view/ViewContext.java +++ b/api/src/org/labkey/api/view/ViewContext.java @@ -164,9 +164,14 @@ 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()) + if (HttpView.hasCurrentView()) return new StackResetter(HttpView.currentContext(), HttpView.getStackSize()); else return pushMockViewContext(user, c, url);