diff --git a/src/org/labkey/test/tests/SpecimenTest.java b/src/org/labkey/test/tests/SpecimenTest.java index 0277fc6e05..b91c164d34 100644 --- a/src/org/labkey/test/tests/SpecimenTest.java +++ b/src/org/labkey/test/tests/SpecimenTest.java @@ -31,6 +31,7 @@ import org.labkey.test.components.html.BootstrapMenu; import org.labkey.test.pages.ImportDataPage; import org.labkey.test.pages.study.specimen.ManageNotificationsPage; +import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.LogMethod; import org.labkey.test.util.LoggedParam; @@ -132,6 +133,7 @@ protected void setupRequestabilityRules() protected void doVerifySteps() throws IOException { verifyActorDetails(); + verifySpecimenEventsRedirect(); createRequest(); verifyViews(); verifyAdditionalRequestFields(); @@ -325,6 +327,37 @@ private void verifyActorDetails() DataRegion(getDriver()).withName("SpecimenRequest").waitFor(); } + // Simulate SpecimenForeignKey redirect behavior + @LogMethod (quiet = true) + private void verifySpecimenEventsRedirect() + { + String targetStudyId = getContainerId(); + + // Create an empty second folder (doesn't need to be a study) with guest read. We'll attempt to invoke the + // redirect action from this folder. + String folderName = "Another Study"; + _containerHelper.createSubfolder(getProjectName(), folderName, "Study"); + new ApiPermissionsHelper(this).setSiteGroupPermissions("Guests", "Reader"); + + // Happy path - admin should redirect + String baseUrl = WebTestHelper.getBaseURL() + "/" + getProjectName() + "/" + folderName + "/specimen-specimenEventsRedirect.view?targetStudy=" + targetStudyId + "&id="; + String url = baseUrl + "AAA07XK5-01"; + beginAt(url); + assertTextPresent("Vial History", "999320812", "350V0600294A"); + + // Guest has access in Another Study but not in the target study (My Study), so should not redirect + signOut(); + beginAt(baseUrl + "abcdefg_123456"); // Bogus ID and user doesn't have read permission + assertTextPresent("Unable to resolve the Specimen ID and target Study"); + beginAt(url); // Valid ID, but user doesn't have read permission to target study, so same error + assertTextPresent("Unable to resolve the Specimen ID and target Study"); + + // Sign in and back to the main study + signIn(); + goToProjectHome(); + clickFolder(getFolderName()); + } + @LogMethod private void createRequest() { @@ -1048,7 +1081,7 @@ private void verifyDrawTimestampConflict(String qcControl, String timestamp, Str { if (StringUtils.isBlank(qcControl)) { - // no conflict, so all three fields shold be valid + // no conflict, so all three fields should be valid assertTrue(StringUtils.isNotBlank(timestamp)); assertTrue(StringUtils.isNotBlank(time)); assertTrue(StringUtils.isNotBlank(date)); diff --git a/src/org/labkey/test/tests/filecontent/UpdateFilePropsContainerScopeTest.java b/src/org/labkey/test/tests/filecontent/UpdateFilePropsContainerScopeTest.java new file mode 100644 index 0000000000..a6c8732293 --- /dev/null +++ b/src/org/labkey/test/tests/filecontent/UpdateFilePropsContainerScopeTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed 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.labkey.test.tests.filecontent; + +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.SimplePostCommand; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.TestFileUtils; +import org.labkey.test.WebTestHelper; +import org.labkey.test.categories.Daily; +import org.labkey.test.categories.FileBrowser; +import org.labkey.test.components.DomainDesignerPage; +import org.labkey.test.util.PortalHelper; +import org.labkey.test.util.core.webdav.WebDavUtils; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@Category({Daily.class, FileBrowser.class}) +@BaseWebDriverTest.ClassTimeout(minutes = 4) +public class UpdateFilePropsContainerScopeTest extends BaseWebDriverTest +{ + private static final String FOLDER_A = "FolderA"; + private static final String FOLDER_B = "FolderB"; + private static final String CUSTOM_PROPERTY = "CustomProp"; + + @Override + protected @Nullable String getProjectName() + { + return getClass().getSimpleName() + "Project"; + } + + @Override + public List getAssociatedModules() + { + return Arrays.asList("filecontent"); + } + + @BeforeClass + public static void doSetup() + { + UpdateFilePropsContainerScopeTest init = getCurrentTest(); + init.doSetupSteps(); + } + + @Override + protected void doCleanup(boolean afterTest) + { + _containerHelper.deleteProject(getProjectName(), afterTest); + } + + private void doSetupSteps() + { + _containerHelper.createProject(getProjectName(), null); + _containerHelper.createSubfolder(getProjectName(), FOLDER_A); + _containerHelper.createSubfolder(getProjectName(), FOLDER_B); + + // FolderA needs a file properties domain so UpdateFilePropsAction's validation block runs. + navigateToFolder(getProjectName(), FOLDER_A); + new PortalHelper(this).doInAdminMode(p -> p.addWebPart("Files")); + DomainDesignerPage designer = _fileBrowserHelper.goToEditProperties(); + designer.fieldsPanel().addField(CUSTOM_PROPERTY); + designer.clickFinish(); + + navigateToFolder(getProjectName(), FOLDER_B); + new PortalHelper(this).doInAdminMode(p -> p.addWebPart("Files")); + } + + @Test + public void testForeignContainerFileRejected() throws Exception + { + final File localFile = TestFileUtils.getSampleData("security/InlineFile.html"); + navigateToFolder(getProjectName(), FOLDER_A); + _fileBrowserHelper.uploadFile(localFile); + String localFileUrl = WebDavUtils.buildBaseWebDavUrl(getProjectName() + "/" + FOLDER_A) + localFile.getName(); + + goToProjectHome(); + final File foreignFile = TestFileUtils.getSampleData("security/InlineFile2.html"); + navigateToFolder(getProjectName(), FOLDER_B); + _fileBrowserHelper.uploadFile(foreignFile); + String foreignFileUrl = WebDavUtils.buildBaseWebDavUrl(getProjectName() + "/" + FOLDER_B) + foreignFile.getName(); + + log("Same-container file id should be accepted."); + updateFileProps(FOLDER_A, localFileUrl, localFile.getName()); + + log("Foreign-container file id should be rejected."); + try + { + updateFileProps(FOLDER_A, foreignFileUrl, foreignFile.getName()); + fail("Expected rejection: UpdateFilePropsAction must refuse a file id resolving outside the current folder."); + } + catch (CommandException ex) + { + assertTrue("Expected 'Invalid file' in error message, got: " + ex.getMessage(), + ex.getMessage().contains("Invalid file")); + } + } + + private void updateFileProps(String folder, String fileId, String fileName) throws Exception + { + JSONObject entry = new JSONObject(); + entry.put("id", fileId); + entry.put("name", fileName); + entry.put(CUSTOM_PROPERTY, "value"); + JSONArray files = new JSONArray(); + files.put(entry); + JSONObject body = new JSONObject(); + body.put("files", files); + + SimplePostCommand cmd = new SimplePostCommand("filecontent", "updateFileProps"); + cmd.setJsonObject(body); + cmd.execute(WebTestHelper.getRemoteApiConnection(), getProjectName() + "/" + folder); + } + + @Override + public BrowserType bestBrowser() + { + return BrowserType.CHROME; + } +}