From a3a4c926e954ed8bbef52731e7acd4d4488be385 Mon Sep 17 00:00:00 2001 From: XingY Date: Thu, 24 Jul 2025 12:46:17 -0700 Subject: [PATCH 01/12] Selenium tests --- src/org/labkey/test/tests/SampleTypeTest.java | 194 +++++++++++++++--- .../labkey/test/util/FileBrowserHelper.java | 55 +++++ 2 files changed, 215 insertions(+), 34 deletions(-) diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index d294a853f2..1ccc1eec5d 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -55,6 +55,7 @@ import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.ExcelHelper; +import org.labkey.test.util.FileBrowserHelper; import org.labkey.test.util.PortalHelper; import org.labkey.test.util.SampleTypeHelper; import org.labkey.test.util.TestDataGenerator; @@ -1582,58 +1583,183 @@ public void testFileAttachment() @Test // Issue 49830 public void testFilePathOnBulkImport() throws IOException { - projectMenu().navigateToFolder(PROJECT_NAME, FOLDER_NAME); + goToProjectHome(); - String sampleTypeName = "FilePathValidation"; String fileFieldName = "FileField"; SampleTypeHelper sampleHelper = new SampleTypeHelper(this); - sampleHelper.createSampleType(new SampleTypeDefinition(sampleTypeName).setFields( + String sampleTypeNameHome = "FilePathValidationHome"; + sampleHelper.createSampleType(new SampleTypeDefinition(sampleTypeNameHome).setFields( + List.of(new FieldDefinition(fileFieldName, ColumnType.File)) + )); + + projectMenu().navigateToFolder(PROJECT_NAME, FOLDER_NAME); + + String sampleTypeNameSub = "FilePathValidationSub"; + sampleHelper.createSampleType(new SampleTypeDefinition(sampleTypeNameSub).setFields( List.of(new FieldDefinition(fileFieldName, ColumnType.File)) )); // add a file system file that isn't under the current container dir, i.e. in the parent dir goToProjectHome(); goToModule("FileContent"); - _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData("sampleType.xlsx")); - // go back to subfolder and import data with relative path that shouldn't resolve - DataRegionTable drt = importSampleTypeFilePathData(sampleTypeName, fileFieldName, "Test1", "../sampleType.xlsx"); - checker().verifyEquals("Sample name in data row not as expected", "Test1", drt.getDataAsText(0, "Name")); - checker().verifyEquals("File field should be empty as path was invalid", " ", drt.getDataAsText(0, fileFieldName)); - - // add a file system file in current container dir and import data with relative path that should resolve + String testFileHomeName = "Update_Lineage_A.tsv"; + String testFileHomeNameB = "Update_Lineage_B.tsv"; + String homeFileDirectory = "homeDir1"; + _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData(testFileHomeName)); + _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData(testFileHomeNameB)); + _fileBrowserHelper.createFolder(homeFileDirectory); + FileBrowserHelper.FileDetailInfo homeFileInfo = _fileBrowserHelper.getFileDetailInfo(PROJECT_NAME, testFileHomeName); + FileBrowserHelper.FileDetailInfo homeFileBInfo = _fileBrowserHelper.getFileDetailInfo(PROJECT_NAME, testFileHomeNameB); + FileBrowserHelper.FileDetailInfo homeDirInfo = _fileBrowserHelper.getFileDetailInfo(PROJECT_NAME, homeFileDirectory); + + String folderContainerPath = PROJECT_NAME + "/" + FOLDER_NAME; + String testFileSubName = "sampleType.tsv"; + String subFileDirectory = "subDir1"; + goToProjectFolder(PROJECT_NAME, FOLDER_NAME); goToModule("FileContent"); - _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData("sampleType.xlsx")); - drt = importSampleTypeFilePathData(sampleTypeName, fileFieldName, "Test2", "sampleType.xlsx"); - checker().verifyEquals("Sample name in data row not as expected", "Test2", drt.getDataAsText(0, "Name")); - checker().verifyEquals("File field should contain file name", " sampleType.xlsx", drt.getDataAsText(0, fileFieldName)); - - // try an import with a valid file that isn't accessible from this container - File propFile = new File(TestFileUtils.getTestRoot(), "test.properties"); - drt = importSampleTypeFilePathData(sampleTypeName, fileFieldName, "Test3", propFile.getAbsolutePath()); - checker().verifyEquals("Sample name in data row not as expected", "Test3", drt.getDataAsText(0, "Name")); - String actualValue = drt.getDataAsText(0, fileFieldName); - checker().verifyTrue("File field should not be valid", " ".equals(actualValue) || actualValue.contains("properties (unavailable)")); - - // try an import with an invalid file path - drt = importSampleTypeFilePathData(sampleTypeName, fileFieldName, "Test4", "invalid/path/to/file"); - checker().verifyEquals("Sample name in data row not as expected", "Test4", drt.getDataAsText(0, "Name")); - checker().verifyTrue("File field should not be valid", drt.getDataAsText(0, fileFieldName).contains("file (unavailable)")); + _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData(testFileSubName)); + _fileBrowserHelper.createFolder(subFileDirectory); + FileBrowserHelper.FileDetailInfo subFileInfo = _fileBrowserHelper.getFileDetailInfo(folderContainerPath, testFileSubName); + FileBrowserHelper.FileDetailInfo subDirInfo = _fileBrowserHelper.getFileDetailInfo(folderContainerPath, subFileDirectory); + + goToProjectHome(); + clickAndWait(Locator.linkWithText(sampleTypeNameHome)); + DataRegionTable drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + ImportDataPage importDataPage = drt.clickImportBulkData(); + + // error cases for home sample type: + // importing directory that does exist under current project root into project + importSampleTypeFilePathDataError("Fail", homeDirInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("Fail", homeDirInfo.webDavUrl()); + importSampleTypeFilePathDataError("Fail", homeDirInfo.dataFileUrl()); + importSampleTypeFilePathDataError("Fail", homeDirInfo.webDavUrlRelative()); + importSampleTypeFilePathDataError("Fail", homeDirInfo.fileName()); + // importing directory that's not under current project root + importSampleTypeFilePathDataError("Fail", "/"); + importSampleTypeFilePathDataError("Fail", "../"); + importSampleTypeFilePathDataError("Fail", "../@files"); + importSampleTypeFilePathDataError("Fail", subDirInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("Fail", subDirInfo.webDavUrl()); + importSampleTypeFilePathDataError("Fail", subDirInfo.dataFileUrl()); + importSampleTypeFilePathDataError("Fail", subDirInfo.webDavUrlRelative()); + // importing file that does exist, but not under current root + importSampleTypeFilePathDataError("Fail", subFileInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("Fail", subFileInfo.webDavUrl()); + importSampleTypeFilePathDataError("Fail", subFileInfo.dataFileUrl()); + importSampleTypeFilePathDataError("Fail", "../" + FOLDER_NAME + "/@files/" + subDirInfo.webDavUrlRelative()); + // importing file that does not exist + importSampleTypeFilePathDataError("Fail", homeFileInfo.absoluteFilePath() + "bad"); + importSampleTypeFilePathDataError("Fail", homeFileInfo.webDavUrl() + "bad"); + importSampleTypeFilePathDataError("Fail", homeFileInfo.dataFileUrl() + "bad"); + importSampleTypeFilePathDataError("Fail", homeFileInfo.webDavUrlRelative() + "bad"); + importSampleTypeFilePathDataError("Fail", homeFileInfo.fileName() + "bad"); + // happy cases: create new records using valid relative or absolute file in Project/Child + String header = "Name\t" + fileFieldName + "\n"; + String homeSampleContent = "S-home-fullPath\t" + homeFileInfo.absoluteFilePath() + "\n" + + "S-home-relativeDav\t" + homeFileInfo.webDavUrlRelative() + "\n" + + "S-home-dataUrl\t" + homeFileInfo.dataFileUrl() + "\n" + + "S-home-davUrl\t" + homeFileInfo.webDavUrl() + "\n" + + "S-home-relative\t" + "../@files/" + homeFileInfo.fileName(); + setFormElement(Locator.name("text"), header + homeSampleContent); + clickButton("Submit"); + drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + String fName = " " + homeFileInfo.fileName(); + checker().verifyEqualsSorted("File field not imported as expected", List.of(fName, fName, fName, fName, fName), drt.getColumnDataAsText(fileFieldName)); + // error case for update + importDataPage = drt.clickImportBulkData(); + importDataPage.setCopyPasteMerge(false, true); + importSampleTypeFilePathDataError("S-home-fullPath", homeDirInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("S-home-fullPath", homeDirInfo.fileName()); + importSampleTypeFilePathDataError("S-home-fullPath", "../"); + importSampleTypeFilePathDataError("S-home-fullPath", subDirInfo.webDavUrl()); + importSampleTypeFilePathDataError("S-home-fullPath", subDirInfo.dataFileUrl()); + importSampleTypeFilePathDataError("S-home-fullPath", homeFileInfo.absoluteFilePath() + "bad"); + // happy cases for update + setFormElement(Locator.name("text"), header + homeSampleContent); // no change + clickButton("Submit"); + drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + checker().verifyEqualsSorted("File field not imported as expected", List.of(fName, fName, fName, fName, fName), drt.getColumnDataAsText(fileFieldName)); + importDataPage = drt.clickImportBulkData(); + importDataPage.setCopyPasteMerge(false, true); + String homeSampleUpdateContent = "S-home-fullPath\t" + homeFileBInfo.absoluteFilePath() + "\n" + + "S-home-relativeDav\t\n" + + "S-home-dataUrl\t" + homeFileBInfo.dataFileUrl() + "\n" + + "S-home-davUrl\t" + homeFileBInfo.webDavUrl() + "\n" + + "S-home-relative\t" + "../@files/" + homeFileBInfo.fileName(); + setFormElement(Locator.name("text"), header + homeSampleUpdateContent); + clickButton("Submit"); + String fNameUpdated = " " + homeFileBInfo.fileName(); + drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + checker().verifyEqualsSorted("File field not imported as expected", List.of(fNameUpdated, fNameUpdated, fNameUpdated, " "/*removed*/, fNameUpdated), drt.getColumnDataAsText(fileFieldName)); + // error case for merge + importDataPage = drt.clickImportBulkData(); + importDataPage.setCopyPasteMerge(true, true); + importSampleTypeFilePathDataError("S-home-fullPath", homeDirInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("S-home-fullPath", subDirInfo.webDavUrl()); + importSampleTypeFilePathDataError("Bad", subDirInfo.webDavUrlRelative()); + // happy case for merge + String homeSampleMergeContent = homeSampleContent + + "\nS-home-merge1\t" + "../@files/" + homeFileBInfo.fileName(); + setFormElement(Locator.name("text"), header + homeSampleMergeContent); + clickButton("Submit"); + drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + checker().verifyEqualsSorted("File field not imported as expected", List.of(fNameUpdated, fName, fName, fName, fName, fName), drt.getColumnDataAsText(fileFieldName)); + + // error cases for child sample type + goToProjectFolder(PROJECT_NAME, FOLDER_NAME); + clickAndWait(Locator.linkWithText(sampleTypeNameSub)); + drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + importDataPage = drt.clickImportBulkData(); + // import data in subfolder with home folder file absolute path, or invalid relative path, or directory + importSampleTypeFilePathDataError("Fail", homeFileInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("Fail", homeFileInfo.webDavUrl()); + importSampleTypeFilePathDataError("Fail", homeFileInfo.dataFileUrl()); + importSampleTypeFilePathDataError("Fail", "../" + testFileHomeName); + importSampleTypeFilePathDataError("Fail", "../../" + testFileHomeName); + importSampleTypeFilePathDataError("Fail", "../"); + importSampleTypeFilePathDataError("Fail", "../../@files"); + importSampleTypeFilePathDataError("Fail", "../../@files/" + homeFileDirectory); + importSampleTypeFilePathDataError("Fail", homeDirInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("Fail", homeDirInfo.webDavUrl()); + importSampleTypeFilePathDataError("Fail", homeDirInfo.dataFileUrl()); + // import data in subfolder with directory that's under current root + importSampleTypeFilePathDataError("Fail", subDirInfo.absoluteFilePath()); + importSampleTypeFilePathDataError("Fail", subDirInfo.webDavUrl()); + importSampleTypeFilePathDataError("Fail", subDirInfo.dataFileUrl()); + importSampleTypeFilePathDataError("Fail", subDirInfo.webDavUrlRelative()); + importSampleTypeFilePathDataError("Fail", subDirInfo.fileName()); + // happy case for creating child sample + String childSampleContent = "S-child-fullPath\t" + subFileInfo.absoluteFilePath() + "\n" + + "S-child-relativeDav\t" + subFileInfo.webDavUrlRelative() + "\n" + + "S-child-dataUrl\t" + subFileInfo.dataFileUrl() + "\n" + + "S-child-davUrl\t" + subFileInfo.webDavUrl() + "\n" + + "S-child-relative\t" + "../@files/" + subFileInfo.fileName(); + setFormElement(Locator.name("text"), header + childSampleContent); + clickButton("Submit"); + drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + fName = " " + subFileInfo.fileName(); + checker().verifyEqualsSorted("File field not imported as expected", List.of(fName, fName, fName, fName, fName), drt.getColumnDataAsText(fileFieldName)); } - private DataRegionTable importSampleTypeFilePathData(String sampleTypeName, String fileFieldName, String sampleName, String filePath) + + private void importSampleTypeFilePathDataError(String sampleName, String filePath) { - projectMenu().navigateToFolder(PROJECT_NAME, FOLDER_NAME); - clickAndWait(Locator.linkWithText(sampleTypeName)); - DataRegionTable drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); - drt.clickImportBulkData(); + final String fileFieldName = "FileField"; String header = "Name\t" + fileFieldName + "\n"; String data = sampleName + "\t" + filePath + "\n"; setFormElement(Locator.name("text"), header + data); - clickButton("Submit"); - return DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + new ImportDataPage(getDriver()).submitExpectingError(); + try + { + waitForElementToBeVisible(Locator.xpath("//div[contains(@class, 'labkey-error')][contains(text(),'Invalid file path: " + filePath + "')]")); + } + catch(NoSuchElementException nse) + { + checker().fatal().error("Invalid file path error not present."); + } } - + @Test public void testCreateViaScript() { diff --git a/src/org/labkey/test/util/FileBrowserHelper.java b/src/org/labkey/test/util/FileBrowserHelper.java index c01e1746b8..8091635f16 100644 --- a/src/org/labkey/test/util/FileBrowserHelper.java +++ b/src/org/labkey/test/util/FileBrowserHelper.java @@ -20,11 +20,17 @@ import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; import org.junit.Assert; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.query.Filter; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.SortDirection; import org.labkey.test.TestProperties; import org.labkey.test.WebDriverWrapper; +import org.labkey.test.WebTestHelper; import org.labkey.test.components.DomainDesignerPage; import org.labkey.test.components.ext4.Checkbox; import org.labkey.test.components.ext4.RadioButton; @@ -40,6 +46,7 @@ import org.openqa.selenium.support.ui.WebDriverWait; import java.io.File; +import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -873,6 +880,54 @@ public void openFolderTree() } } + private String stringOrNull(Object value) + { + if (value == null) + return null; + return (String) value; + } + + public record FileDetailInfo(String fileName, String absoluteFilePath, String dataFileUrl, String webDavUrl, String webDavUrlRelative) + { + + } + + public FileDetailInfo getFileDetailInfo(String containerPath, String fileName) + { + List filePathColumns = List.of("AbsoluteFilePath", "DataFileUrl", "WebDavUrl", "WebDavUrlRelative"); + try + { + Connection cn = WebTestHelper.getRemoteApiConnection(); + SelectRowsCommand cmd = new SelectRowsCommand("exp", "files"); + cmd.addFilter("Name", fileName, Filter.Operator.EQUAL); + cmd.setColumns(filePathColumns); + SelectRowsResponse response = cmd.execute(cn, "/" + containerPath); + + Map row = response.getRows().get(0); + Object absoluteFilePath = row.get("AbsoluteFilePath"); + Object dataFileUrl = row.get("DataFileUrl"); + Object webDavUrl = row.get("WebDavUrl"); + Object webDavUrlRelative = row.get("WebDavUrlRelative"); + return new FileDetailInfo(fileName, stringOrNull(absoluteFilePath), stringOrNull(dataFileUrl), stringOrNull(webDavUrl), stringOrNull(webDavUrlRelative)); + } + catch (CommandException ce) + { + if (ce.getStatusCode() == 404) + { + return null; + } + else + { + throw new RuntimeException(ce); + } + } + catch (IOException ioe) + { + throw new RuntimeException(ioe); + } + + } + // See PageFlowUtil.encodeURIComponent() private static final Map DECODE_UNRESERVED_MARKS = Map.of( "!", "%21", From f5d54160c4ed237b5e827add13ea697ad7c58157 Mon Sep 17 00:00:00 2001 From: XingY Date: Fri, 25 Jul 2025 09:03:01 -0700 Subject: [PATCH 02/12] Fix assayfile import --- src/org/labkey/test/tests/SampleTypeTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index 1ccc1eec5d..3e5e232006 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -51,11 +51,13 @@ import org.labkey.test.params.FieldDefinition.LookupInfo; import org.labkey.test.params.FieldInfo; import org.labkey.test.params.experiment.SampleTypeDefinition; +import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionExportHelper; import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.ExcelHelper; import org.labkey.test.util.FileBrowserHelper; +import org.labkey.test.util.PasswordUtil; import org.labkey.test.util.PortalHelper; import org.labkey.test.util.SampleTypeHelper; import org.labkey.test.util.TestDataGenerator; @@ -1583,6 +1585,9 @@ public void testFileAttachment() @Test // Issue 49830 public void testFilePathOnBulkImport() throws IOException { + new ApiPermissionsHelper(this) + .setSiteRoleUserPermissions(PasswordUtil.getUsername(), "See Absolute File Paths"); + goToProjectHome(); String fileFieldName = "FileField"; From cbc7ef48903d51502fe61888535c272145696629 Mon Sep 17 00:00:00 2001 From: XingY Date: Sat, 26 Jul 2025 19:28:45 -0700 Subject: [PATCH 03/12] assay api tests --- src/org/labkey/test/util/EscapeUtil.java | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/org/labkey/test/util/EscapeUtil.java b/src/org/labkey/test/util/EscapeUtil.java index 8e7241219e..b5ea40ad83 100644 --- a/src/org/labkey/test/util/EscapeUtil.java +++ b/src/org/labkey/test/util/EscapeUtil.java @@ -28,6 +28,31 @@ public class EscapeUtil { + static public String toJSONStr(String str) + { + if (str == null) return null; + StringBuilder escaped = new StringBuilder(); + for (char c : str.toCharArray()) { + switch (c) { + case '"': escaped.append("\\\""); break; + case '\\': escaped.append("\\\\"); break; + case '\b': escaped.append("\\b"); break; + case '\f': escaped.append("\\f"); break; + case '\n': escaped.append("\\n"); break; + case '\r': escaped.append("\\r"); break; + case '\t': escaped.append("\\t"); break; + default: + // Escape control characters (ASCII 0-31) and ensure Unicode compatibility + if (c < 32) { + escaped.append(String.format("\\u%04x", (int) c)); + } else { + escaped.append(c); + } + } + } + return escaped.toString(); + } + static public String jsString(String s) { if (s == null) From 92642b7c3f0d3f2760c10a6e2fb3607586770998 Mon Sep 17 00:00:00 2001 From: XingY Date: Sat, 26 Jul 2025 21:20:04 -0700 Subject: [PATCH 04/12] add test coverage for study dataset --- .../labkey/test/util/FileBrowserHelper.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/org/labkey/test/util/FileBrowserHelper.java b/src/org/labkey/test/util/FileBrowserHelper.java index 8091635f16..b4ac3b149e 100644 --- a/src/org/labkey/test/util/FileBrowserHelper.java +++ b/src/org/labkey/test/util/FileBrowserHelper.java @@ -889,12 +889,11 @@ private String stringOrNull(Object value) public record FileDetailInfo(String fileName, String absoluteFilePath, String dataFileUrl, String webDavUrl, String webDavUrlRelative) { - } public FileDetailInfo getFileDetailInfo(String containerPath, String fileName) { - List filePathColumns = List.of("AbsoluteFilePath", "DataFileUrl", "WebDavUrl", "WebDavUrlRelative"); + List filePathColumns = List.of("AbsoluteFilePath", "FileExists", "DataFileUrl", "WebDavUrl", "WebDavUrlRelative"); try { Connection cn = WebTestHelper.getRemoteApiConnection(); @@ -903,12 +902,16 @@ public FileDetailInfo getFileDetailInfo(String containerPath, String fileName) cmd.setColumns(filePathColumns); SelectRowsResponse response = cmd.execute(cn, "/" + containerPath); - Map row = response.getRows().get(0); - Object absoluteFilePath = row.get("AbsoluteFilePath"); - Object dataFileUrl = row.get("DataFileUrl"); - Object webDavUrl = row.get("WebDavUrl"); - Object webDavUrlRelative = row.get("WebDavUrlRelative"); - return new FileDetailInfo(fileName, stringOrNull(absoluteFilePath), stringOrNull(dataFileUrl), stringOrNull(webDavUrl), stringOrNull(webDavUrlRelative)); + for (Map row: response.getRows()) + { + if (!(Boolean) row.get("FileExists")) + continue; + Object absoluteFilePath = row.get("AbsoluteFilePath"); + Object dataFileUrl = row.get("DataFileUrl"); + Object webDavUrl = row.get("WebDavUrl"); + Object webDavUrlRelative = row.get("WebDavUrlRelative"); + return new FileDetailInfo(fileName, stringOrNull(absoluteFilePath), stringOrNull(dataFileUrl), stringOrNull(webDavUrl), stringOrNull(webDavUrlRelative)); + } } catch (CommandException ce) { @@ -926,6 +929,7 @@ public FileDetailInfo getFileDetailInfo(String containerPath, String fileName) throw new RuntimeException(ioe); } + return null; } // See PageFlowUtil.encodeURIComponent() From 8ec75aaa1c0673b9853ef226058d76b6272f40cf Mon Sep 17 00:00:00 2001 From: XingY Date: Sun, 27 Jul 2025 17:47:44 -0700 Subject: [PATCH 05/12] fix various assay api, add tests for assay api --- src/org/labkey/test/AssayAPITest.java | 85 +++++++++++++++----- src/org/labkey/test/util/APIAssayHelper.java | 64 +++++++++++++-- 2 files changed, 122 insertions(+), 27 deletions(-) diff --git a/src/org/labkey/test/AssayAPITest.java b/src/org/labkey/test/AssayAPITest.java index c01c17812b..2b696c4e2f 100644 --- a/src/org/labkey/test/AssayAPITest.java +++ b/src/org/labkey/test/AssayAPITest.java @@ -18,6 +18,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; +import org.labkey.api.util.Pair; import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.assay.GetProtocolCommand; @@ -31,8 +32,11 @@ import org.labkey.test.pages.ReactAssayDesignerPage; import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.APIAssayHelper; +import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.FileBrowserHelper; import org.labkey.test.util.Maps; +import org.labkey.test.util.PasswordUtil; import org.labkey.test.util.UIAssayHelper; import java.io.File; @@ -247,6 +251,9 @@ private void createAssayWithFileFields(String assayName) @Test public void testImportRun_dataRows() throws Exception { + new ApiPermissionsHelper(this) + .setSiteRoleUserPermissions(PasswordUtil.getUsername(), "See Absolute File Paths"); + goToProjectHome(); log("create GPAT assay"); @@ -273,22 +280,48 @@ public void testImportRun_dataRows() throws Exception assertElementPresent("Did not find the expected number of icons for images for " + SCREENSHOT_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + SCREENSHOT_FILE.getName() + "')]"), 1); assertElementPresent("Did not find the expected number of icons for images for " + FOO_XLS_FILE.getName() + " from the runs.", Locator.xpath("//a[contains(text(), '" + FOO_XLS_FILE.getName() + "')]"), 2); - log("verify files can be resolved after the run is imported"); String runName = "file resolution run"; - dataRows = Arrays.asList( - Maps.of("ptid", "p03", "date", "2017-05-10", "DataFileField", "crest-2.png") + List> dataRowsInvalidResultFileName = Arrays.asList( + Maps.of("ptid", "p03", "date", "2017-05-10", "DataFileField", CREST_2_FILE.getName()) + ); + List> dataRowsInvalidResultFileAbsolutePath = Arrays.asList( + Maps.of("ptid", "p03", "date", "2017-05-10", "DataFileField", CREST_2_FILE.getAbsolutePath()) + ); + List> dataRowsInvalidResultFileDirectory = Arrays.asList( + Maps.of("ptid", "p03", "date", "2017-05-10", "DataFileField", "../") ); - // import the file using a relative path - resp = assayHelper.importAssay(assayId, runName, dataRows, getProjectName(), Collections.singletonMap("RunFileField", "crest-2.png"), Collections.emptyMap()); - beginAt(resp.getSuccessURL()); - assertElementNotPresent("File should not exist for " + CREST_2_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + CREST_2_FILE.getName() + "')]")); + log("verify invalid file path is rejected during import"); + // invalid run file and result file + assayHelper.importAssay(assayId, runName, dataRowsInvalidResultFileName, getProjectName(), Collections.singletonMap("RunFileField", CREST_2_FILE.getName()), Collections.emptyMap(), "Invalid file path: crest-2.png"); + assayHelper.importAssay(assayId, runName, dataRowsInvalidResultFileName, getProjectName(), Collections.singletonMap("RunFileField", CREST_2_FILE.getAbsolutePath()), Collections.emptyMap(), "Invalid file path: " + CREST_2_FILE.getAbsolutePath()); + assayHelper.importAssay(assayId, runName, dataRowsInvalidResultFileName, getProjectName(), Collections.singletonMap("RunFileField", "../"), Collections.emptyMap(), "Invalid file path: ../"); + // valid run file but invalid result file + assayHelper.importAssay(assayId, runName, dataRowsInvalidResultFileName, getProjectName(), Collections.singletonMap("RunFileField", CREST_FILE.getName()), Collections.emptyMap(), "Invalid file path: crest-2.png"); + assayHelper.importAssay(assayId, runName, dataRowsInvalidResultFileAbsolutePath, getProjectName(), Collections.singletonMap("RunFileField", CREST_FILE.getName()), Collections.emptyMap(), "Invalid file path: " + CREST_2_FILE.getAbsolutePath()); + assayHelper.importAssay(assayId, runName, dataRowsInvalidResultFileDirectory, getProjectName(), Collections.singletonMap("RunFileField", CREST_FILE.getName()), Collections.emptyMap(), "Invalid file path: ../"); + + // valid run file and valid result file + FileBrowserHelper.FileDetailInfo runFileInfo = _fileBrowserHelper.getFileDetailInfo(getProjectName(), CREST_FILE.getName()); + FileBrowserHelper.FileDetailInfo resultFileInfo = _fileBrowserHelper.getFileDetailInfo(getProjectName(), SCREENSHOT_FILE.getName()); + List> scenarios = List.of(new Pair<>(CREST_FILE.getName(), SCREENSHOT_FILE.getName()), + new Pair<>(runFileInfo.absoluteFilePath(), resultFileInfo.absoluteFilePath()), + new Pair<>(runFileInfo.webDavUrl(), resultFileInfo.webDavUrl()), + new Pair<>(runFileInfo.dataFileUrl(), resultFileInfo.dataFileUrl()), + new Pair<>(runFileInfo.webDavUrlRelative(), resultFileInfo.webDavUrlRelative())); + int count = 3; + for (Pair scenario : scenarios) + { + List> dataRowsValid = Arrays.asList(Maps.of("ptid", "p0" + count++, "date", "2017-05-10", "DataFileField", scenario.second)); + assayHelper.importAssay(assayId, "ValidPath" + count, dataRowsValid, getProjectName(), Collections.singletonMap("RunFileField", scenario.first), Collections.emptyMap()); + } - goToModule("FileContent"); - _fileBrowserHelper.uploadFile(CREST_2_FILE); - beginAt(resp.getSuccessURL()); - assertElementPresent("Did not find the expected number of icons for " + CREST_2_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + CREST_2_FILE.getName() + "')]"), 2); + clickAndWait(Locator.linkContainingText(assayName)); + clickAndWait(Locator.linkContainingText("view runs")); + assertElementPresent("Did not find the expected number of icons for " + CREST_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + CREST_FILE.getName() + "')]"), 5); + clickAndWait(Locator.linkContainingText("view results")); + assertElementPresent("Did not find the expected number of icons for " + SCREENSHOT_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + SCREENSHOT_FILE.getName() + "')]"), 6); } @@ -296,6 +329,9 @@ public void testImportRun_dataRows() throws Exception @Test public void testGpatSaveBatch() throws Exception { + new ApiPermissionsHelper(this) + .setSiteRoleUserPermissions(PasswordUtil.getUsername(), "See Absolute File Paths"); + goToProjectHome(); log("create GPAT assay"); @@ -308,7 +344,7 @@ public void testGpatSaveBatch() throws Exception resultRows.add(Maps.of("ptid", "188438418", "SpecimenID", "K770K3VY-19", "DataFileField", "crest.png")); resultRows.add(Maps.of("ptid", "188487431", "SpecimenID", "A770K4W1-15", "DataFileField", "screenshot.png")); - ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "foo.xls"), resultRows, getProjectName()); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "foo.xls"), resultRows, getProjectName(), null); log("verify assay saveBatch worked"); goToManageAssays(); @@ -322,23 +358,30 @@ public void testGpatSaveBatch() throws Exception assertElementPresent("Did not find the expected number of icons for images for " + SCREENSHOT_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + SCREENSHOT_FILE.getName() + "')]"), 1); assertElementPresent("Did not find the expected number of icons for images for " + FOO_XLS_FILE.getName() + " from the runs.", Locator.xpath("//a[contains(text(), '" + FOO_XLS_FILE.getName() + "')]"), 2); - log("verify files can be resolved after the run is imported"); + runName = "invalid run file path"; resultRows.clear(); resultRows.add(Maps.of("ptid", "188438419", "SpecimenID", "K770K3VY-20", "DataFileField", "help.jpg")); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "help.jpg"), resultRows, getProjectName(), "Invalid file path: help.jpg"); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", HELP_ICON_FILE.getAbsolutePath()), resultRows, getProjectName(), "Invalid file path: " + HELP_ICON_FILE.getAbsolutePath()); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", CREST_FILE.getAbsolutePath()), resultRows, getProjectName(), "Invalid file path: " + CREST_FILE.getAbsolutePath()); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "../"), resultRows, getProjectName(), "Invalid file path: ../"); - runName = "file resolution run"; - ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "help.jpg"), resultRows, getProjectName()); - goToManageAssays(); - clickAndWait(Locator.linkContainingText(assayName)); - clickAndWait(Locator.linkContainingText(runName)); - assertElementNotPresent("File should not exist for " + HELP_ICON_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + HELP_ICON_FILE.getName() + "')]")); + runName = "valid run file path, invalid result file path"; + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", CREST_FILE.getName()), resultRows, getProjectName(), "Invalid file path: help.jpg"); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", CREST_FILE.getName()), List.of(Maps.of("ptid", "188438419", "SpecimenID", "K770K3VY-20", "DataFileField", CREST_FILE.getAbsolutePath())), getProjectName(), "Invalid file path: " + CREST_FILE.getAbsolutePath()); goToModule("FileContent"); _fileBrowserHelper.uploadFile(HELP_ICON_FILE); goToManageAssays(); + FileBrowserHelper.FileDetailInfo runFileInfo = _fileBrowserHelper.getFileDetailInfo(getProjectName(), "help.jpg"); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, "Valid absolute path", Collections.singletonMap("RunFileField", runFileInfo.absoluteFilePath()), resultRows, getProjectName(), null); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, "Valid webdav full path", Collections.singletonMap("RunFileField", runFileInfo.webDavUrl()), resultRows, getProjectName(), null); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, "Valid webdav relative path", Collections.singletonMap("RunFileField", runFileInfo.webDavUrlRelative()), resultRows, getProjectName(), null); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, "Valid data file url", Collections.singletonMap("RunFileField", runFileInfo.dataFileUrl()), resultRows, getProjectName(), null); + clickAndWait(Locator.linkContainingText(assayName)); - clickAndWait(Locator.linkContainingText(runName)); - assertElementPresent("Did not find the expected number of icons for " + HELP_ICON_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + HELP_ICON_FILE.getName() + "')]"), 2); + clickAndWait(Locator.linkContainingText("view runs")); + assertElementPresent("Did not find the expected number of icons for " + HELP_ICON_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + HELP_ICON_FILE.getName() + "')]"), 4); } @Override diff --git a/src/org/labkey/test/util/APIAssayHelper.java b/src/org/labkey/test/util/APIAssayHelper.java index cec86cfd4f..e02849f963 100644 --- a/src/org/labkey/test/util/APIAssayHelper.java +++ b/src/org/labkey/test/util/APIAssayHelper.java @@ -52,6 +52,7 @@ import java.util.List; import java.util.Map; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; public class APIAssayHelper extends AbstractAssayHelper @@ -83,14 +84,44 @@ public ImportRunResponse importAssay(int assayID, String runFilePath, String pro @LogMethod(quiet = true) public ImportRunResponse importAssay(int assayID, String runName, List> dataRows, String projectPath, - Map runProperties, Map batchProperties) throws CommandException, IOException + Map runProperties, Map batchProperties, String errorMsg) throws CommandException, IOException { ImportRunCommand irc = new ImportRunCommand(assayID, dataRows); irc.setName(runName); irc.setProperties(runProperties); irc.setBatchProperties(batchProperties); irc.setTimeout(180000); // Wait 3 minutes for assay import - return irc.execute(_test.createDefaultConnection(), projectPath); + if (errorMsg != null) + { + try + { + irc.execute(_test.createDefaultConnection(), projectPath); + throw new Exception("This should have failed"); + } + catch (CommandException e) + { + Map responseJson = e.getProperties(); + if (!responseJson.containsKey("exception")) + throw new CommandException("Response lacks exception"); + + String exception = responseJson.get("exception").toString(); + assertEquals("Invalid file path message not as expected", errorMsg, exception); + return null; + } + catch (Exception e) + { + throw new CommandException(e.getMessage()); + } + } + else + return irc.execute(_test.createDefaultConnection(), projectPath); + } + + @LogMethod(quiet = true) + public ImportRunResponse importAssay(int assayID, String runName, List> dataRows, String projectPath, + Map runProperties, Map batchProperties) throws CommandException, IOException + { + return importAssay(assayID, runName, dataRows, projectPath, runProperties, batchProperties, null); } @LogMethod(quiet = true) @@ -280,7 +311,7 @@ public static Map getProtocolIds(String containerPath, Connecti return resultData; } - public void saveBatch(String assayName, String runName, Map runProperties, List> resultRows, String projectName) throws IOException, CommandException + public void saveBatch(String assayName, String runName, Map runProperties, List> resultRows, String projectName, @Nullable String errorMsg) throws Exception { int assayId = getIdFromAssayName(assayName, projectName); @@ -294,14 +325,35 @@ public void saveBatch(String assayName, String runName, Map runP runs.add(run); batch.setRuns(runs); - saveBatch(assayId, batch, projectName); + saveBatch(assayId, batch, projectName, errorMsg); } - public void saveBatch(int assayId, Batch batch, String projectPath) throws IOException, CommandException + public void saveBatch(int assayId, Batch batch, String projectPath, @Nullable String errorMsg) throws Exception { SaveAssayBatchCommand cmd = new SaveAssayBatchCommand(assayId, batch); cmd.setTimeout(180000); // Wait 3 minutes for assay import - cmd.execute(_test.createDefaultConnection(), projectPath); + if (errorMsg != null) + { + try + { + var result = cmd.execute(_test.createDefaultConnection(), projectPath); + throw new Exception("This should have failed"); + } + catch (CommandException e) + { + Map responseJson = e.getProperties(); + if (!responseJson.containsKey("exception")) + throw new Exception("Response lacks exception"); + + String exception = responseJson.get("exception").toString(); + assertEquals("Invalid file path message not as expected", errorMsg, exception); + } + } + else + cmd.execute(_test.createDefaultConnection(), projectPath); + + + } public Protocol createAssayDesignWithDefaults(String containerPath, String providerName, String assayName) throws IOException, CommandException From 5962ab53fc2f364a2f8ab57e042c3a0195255afb Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 28 Jul 2025 11:35:03 -0700 Subject: [PATCH 06/12] Fix tests --- src/org/labkey/test/AssayAPITest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/org/labkey/test/AssayAPITest.java b/src/org/labkey/test/AssayAPITest.java index 2b696c4e2f..f20c282bfc 100644 --- a/src/org/labkey/test/AssayAPITest.java +++ b/src/org/labkey/test/AssayAPITest.java @@ -274,6 +274,8 @@ public void testImportRun_dataRows() throws Exception ImportRunResponse resp = assayHelper.importAssay(assayId, "x", dataRows, getProjectName(), Collections.singletonMap("RunFileField", "foo.xls"), Collections.emptyMap()); beginAt(resp.getSuccessURL()); assertTextPresent("p01", "p02"); + DataRegionTable table = new DataRegionTable("Data", this); + table.clearAllFilters(); // remove run filter // verify images are resolved and rendered properly assertElementPresent("Did not find the expected number of icons for images for " + CREST_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + CREST_FILE.getName() + "')]"), 1); From 882cd2b401ffb3dfe7894784944112e82ac8879c Mon Sep 17 00:00:00 2001 From: XingY Date: Fri, 1 Aug 2025 14:20:41 -0700 Subject: [PATCH 07/12] code review changes --- src/org/labkey/test/tests/SampleTypeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index 20474743d5..97e21ef6fe 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -1631,7 +1631,7 @@ public void testFilePathOnBulkImport() throws IOException goToProjectHome(); clickAndWait(Locator.linkWithText(sampleTypeNameHome)); DataRegionTable drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); - ImportDataPage importDataPage = drt.clickImportBulkData(); + var importDataPage = drt.clickImportBulkData(); // error cases for home sample type: // importing directory that does exist under current project root into project From f462068aa5981f2e99e23428639cd6f49b4fe3ef Mon Sep 17 00:00:00 2001 From: XingY Date: Fri, 1 Aug 2025 19:23:43 -0700 Subject: [PATCH 08/12] Fix assay run api, more cleaning up --- src/org/labkey/test/AssayAPITest.java | 68 +++++++++++++++++++-- src/org/labkey/test/util/APITestHelper.java | 30 +++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/AssayAPITest.java b/src/org/labkey/test/AssayAPITest.java index f20c282bfc..9d47f2552b 100644 --- a/src/org/labkey/test/AssayAPITest.java +++ b/src/org/labkey/test/AssayAPITest.java @@ -15,6 +15,7 @@ */ package org.labkey.test; +import org.jetbrains.annotations.Nullable; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -32,8 +33,10 @@ import org.labkey.test.pages.ReactAssayDesignerPage; import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.APIAssayHelper; +import org.labkey.test.util.APITestHelper; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.FileBrowserHelper; import org.labkey.test.util.Maps; import org.labkey.test.util.PasswordUtil; @@ -51,6 +54,7 @@ import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.labkey.test.params.FieldDefinition.DOMAIN_TRICKY_CHARACTERS; @@ -232,6 +236,8 @@ private void createAssayWithFileFields(String assayName) { ReactAssayDesignerPage assayDesigner = _assayHelper.createAssayDesign("General", assayName); + assayDesigner.setEditableRuns(true); // test updateRows.api + log("Create a 'File' column for the assay run."); assayDesigner.goToRunFields() .addField("RunFileField") @@ -341,17 +347,18 @@ public void testGpatSaveBatch() throws Exception createAssayWithFileFields(assayName); log("create run via saveBatch"); - String runName = "created-via-saveBatch"; + String runNameSaved = "created-via-saveBatch"; List> resultRows = new ArrayList<>(); resultRows.add(Maps.of("ptid", "188438418", "SpecimenID", "K770K3VY-19", "DataFileField", "crest.png")); resultRows.add(Maps.of("ptid", "188487431", "SpecimenID", "A770K4W1-15", "DataFileField", "screenshot.png")); - ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "foo.xls"), resultRows, getProjectName(), null); + ((APIAssayHelper) _assayHelper).saveBatch(assayName, runNameSaved, Collections.singletonMap("RunFileField", "foo.xls"), resultRows, getProjectName(), null); + Integer savedRunId = getRunId(assayName, runNameSaved); log("verify assay saveBatch worked"); goToManageAssays(); clickAndWait(Locator.linkContainingText(assayName)); - clickAndWait(Locator.linkContainingText(runName)); + clickAndWait(Locator.linkContainingText(runNameSaved)); DataRegionTable table = new DataRegionTable("Data", this); assertEquals(Arrays.asList("K770K3VY-19", "A770K4W1-15"), table.getColumnDataAsText("SpecimenID")); @@ -360,7 +367,7 @@ public void testGpatSaveBatch() throws Exception assertElementPresent("Did not find the expected number of icons for images for " + SCREENSHOT_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + SCREENSHOT_FILE.getName() + "')]"), 1); assertElementPresent("Did not find the expected number of icons for images for " + FOO_XLS_FILE.getName() + " from the runs.", Locator.xpath("//a[contains(text(), '" + FOO_XLS_FILE.getName() + "')]"), 2); - runName = "invalid run file path"; + String runName = "invalid run file path"; resultRows.clear(); resultRows.add(Maps.of("ptid", "188438419", "SpecimenID", "K770K3VY-20", "DataFileField", "help.jpg")); ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "help.jpg"), resultRows, getProjectName(), "Invalid file path: help.jpg"); @@ -368,6 +375,12 @@ public void testGpatSaveBatch() throws Exception ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", CREST_FILE.getAbsolutePath()), resultRows, getProjectName(), "Invalid file path: " + CREST_FILE.getAbsolutePath()); ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", "../"), resultRows, getProjectName(), "Invalid file path: ../"); + // update run file using updateRows + verifyUpdateRunFileAPIError(assayName, "RunFileField", savedRunId, "help.jpg"); + verifyUpdateRunFileAPIError(assayName, "RunFileField", savedRunId, HELP_ICON_FILE.getAbsolutePath()); + verifyUpdateRunFileAPIError(assayName, "RunFileField", savedRunId, CREST_FILE.getAbsolutePath()); + verifyUpdateRunFileAPIError(assayName, "RunFileField", savedRunId, "../"); + runName = "valid run file path, invalid result file path"; ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", CREST_FILE.getName()), resultRows, getProjectName(), "Invalid file path: help.jpg"); ((APIAssayHelper) _assayHelper).saveBatch(assayName, runName, Collections.singletonMap("RunFileField", CREST_FILE.getName()), List.of(Maps.of("ptid", "188438419", "SpecimenID", "K770K3VY-20", "DataFileField", CREST_FILE.getAbsolutePath())), getProjectName(), "Invalid file path: " + CREST_FILE.getAbsolutePath()); @@ -384,6 +397,53 @@ public void testGpatSaveBatch() throws Exception clickAndWait(Locator.linkContainingText(assayName)); clickAndWait(Locator.linkContainingText("view runs")); assertElementPresent("Did not find the expected number of icons for " + HELP_ICON_FILE.getName() + " from the runs.", Locator.xpath("//img[contains(@title, '" + HELP_ICON_FILE.getName() + "')]"), 4); + + // verify updateRows successful + verifyUpdateRunFileAPI(assayName, "RunFileField", savedRunId, runFileInfo.absoluteFilePath(), null); + verifyUpdateRunFileAPI(assayName, "RunFileField", savedRunId, runFileInfo.webDavUrl(), null); + verifyUpdateRunFileAPI(assayName, "RunFileField", savedRunId, runFileInfo.webDavUrlRelative(), null); + verifyUpdateRunFileAPI(assayName, "RunFileField", savedRunId, runFileInfo.dataFileUrl(), null); + } + + protected void executeAndVerifyScript(String script, @Nullable String errorMsg) + { + log(script); + Map result = (Map)executeAsyncScript(script); + + String failureResult = APITestHelper.parseScriptResult(result); + + if (errorMsg == null) + assertNull(failureResult); + else + assertEquals("Unexpected error message", errorMsg, result.get("exception")); + } + + private void verifyUpdateRunFileAPI(String assayName, String runFileField, int runRowId, String filePath, String errorMsg) + { + String updateScript = "LABKEY.Query.updateRows({ schemaName: \"assay.General." + EscapeUtil.fieldKeyEncodePart(assayName) + "\", "+ + "queryName: \"Runs\", " + + "success: callback," + + "failure: callback," + + "rows: [{ RowId: \""+ runRowId + "\"," + + "\"" + EscapeUtil.toJSONStr(runFileField) + "\": \"" + filePath + "\"," + + "}]" + + "})"; + executeAndVerifyScript(updateScript, errorMsg); + } + + private void verifyUpdateRunFileAPIError(String assayName, String runFileField, int runRowId, String filePath) + { + verifyUpdateRunFileAPI(assayName, runFileField, runRowId, filePath, "Invalid file path: " + filePath); + } + + private @Nullable Integer getRunId(String assayName, String runName) + { + var rows = executeSelectRowCommand("assay.General." + EscapeUtil.fieldKeyEncodePart(assayName), "Runs").getRows(); + var row = rows.stream().filter(a-> a.get("name").equals(runName)).findFirst().orElse(null); + if (row == null) + return null; + + return (Integer) row.get("rowId"); } @Override diff --git a/src/org/labkey/test/util/APITestHelper.java b/src/org/labkey/test/util/APITestHelper.java index fac7c8ca25..eabf7ce5ff 100644 --- a/src/org/labkey/test/util/APITestHelper.java +++ b/src/org/labkey/test/util/APITestHelper.java @@ -38,6 +38,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import static org.junit.Assert.fail; @@ -206,6 +207,35 @@ public static void injectCookies(@NotNull String username, HttpUriRequest method method.setHeader(session.getName(), session.getValue()); } + /* + reads a script result object, returns a string representation of an exception if it exists. + calling code should check the result and assert accordingly. + an exception means there was a server-side exception, rather than a script error. + */ + static public String parseScriptResult(Map scriptResult) + { + if (scriptResult.containsKey("exception")) + { + String exType = (String)scriptResult.get("exception"); + if (exType.contains("ERROR:")) + return exType; // not an exception, but a friendly error message + + ArrayList frames = (ArrayList)scriptResult.get("stackTrace"); + + StringBuilder builder = new StringBuilder(); + if (null != frames) + { + for (String frame : frames) + { + builder.append(frame + "\n"); + } + return "An exception of type [" + exType + "] occurred while executing the script.\n[ " + builder + " ]"; + } + } + + return null; + } + public static class ApiTestCase { private String _name; From ca28e350b4ec301e0c46fdca80ee7ae77515e5c3 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 4 Aug 2025 14:53:36 -0700 Subject: [PATCH 09/12] fix tests for Windows with quoted path --- src/org/labkey/test/AssayAPITest.java | 2 +- src/org/labkey/test/tests/SampleTypeTest.java | 7 ++++--- src/org/labkey/test/util/data/TestDataUtils.java | 12 ++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/AssayAPITest.java b/src/org/labkey/test/AssayAPITest.java index 9d47f2552b..a3f1945a62 100644 --- a/src/org/labkey/test/AssayAPITest.java +++ b/src/org/labkey/test/AssayAPITest.java @@ -425,7 +425,7 @@ private void verifyUpdateRunFileAPI(String assayName, String runFileField, int r "success: callback," + "failure: callback," + "rows: [{ RowId: \""+ runRowId + "\"," + - "\"" + EscapeUtil.toJSONStr(runFileField) + "\": \"" + filePath + "\"," + + "\"" + EscapeUtil.toJSONStr(runFileField) + "\": \"" + EscapeUtil.toJSONStr(filePath) + "\"," + "}]" + "})"; executeAndVerifyScript(updateScript, errorMsg); diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index 97e21ef6fe..82c2e51843 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -62,6 +62,7 @@ import org.labkey.test.util.SampleTypeHelper; import org.labkey.test.util.TestDataGenerator; import org.labkey.test.util.TestUser; +import org.labkey.test.util.data.TestDataUtils; import org.labkey.test.util.exp.SampleTypeAPIHelper; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; @@ -1751,9 +1752,9 @@ public void testFilePathOnBulkImport() throws IOException private void importSampleTypeFilePathDataError(String sampleName, String filePath) { final String fileFieldName = "FileField"; - String header = "Name\t" + fileFieldName + "\n"; - String data = sampleName + "\t" + filePath + "\n"; - setFormElement(Locator.name("text"), header + data); + String pasteData = TestDataUtils.tsvStringFromRowMapsWithQuote(List.of(Map.of("Name", sampleName, fileFieldName, filePath)), + List.of("Name", fileFieldName), true); + setFormElement(Locator.name("text"), pasteData); new ImportDataPage(getDriver()).submitExpectingError(); try { diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index 8c0a71d50c..f828117545 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -296,6 +296,12 @@ public static String tsvStringFromRowMaps(List> rowMaps, Lis return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.TDF); } + public static String tsvStringFromRowMapsWithQuote(List> rowMaps, List columns, + boolean includeHeaders) + { + return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.MONGODB_TSV); + } + public static List> mapsFromRows(List> allRows) { List> rowMaps = new ArrayList<>(); @@ -439,6 +445,12 @@ public static File writeRowsToTsv(String fileName, List> rows) throw return writeRowsToFile(fileName, rows, CSVFormat.TDF); } + public static File writeRowsToTsvWithQuote(String fileName, List> rows) throws IOException + { + return writeRowsToFile(fileName, rows, CSVFormat.MONGODB_TSV); + } + + public static File writeRowsToCsv(String fileName, List> rows) throws IOException { return writeRowsToFile(fileName, rows, CSVFormat.DEFAULT); From 903be0a6d140dcba6581eb35bf87dead4dfe9f25 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 4 Aug 2025 16:19:35 -0700 Subject: [PATCH 10/12] fix tests for Windows with quoted path --- src/org/labkey/test/tests/SampleTypeTest.java | 2 +- src/org/labkey/test/util/data/TestDataUtils.java | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index 82c2e51843..bb4fb2354e 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -1752,7 +1752,7 @@ public void testFilePathOnBulkImport() throws IOException private void importSampleTypeFilePathDataError(String sampleName, String filePath) { final String fileFieldName = "FileField"; - String pasteData = TestDataUtils.tsvStringFromRowMapsWithQuote(List.of(Map.of("Name", sampleName, fileFieldName, filePath)), + String pasteData = TestDataUtils.tsvStringFromRowMapsEscapeBackslash(List.of(Map.of("Name", sampleName, fileFieldName, filePath)), List.of("Name", fileFieldName), true); setFormElement(Locator.name("text"), pasteData); new ImportDataPage(getDriver()).submitExpectingError(); diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index f828117545..e2ef164a38 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -14,7 +14,6 @@ import org.labkey.serverapi.reader.TabLoader; import org.labkey.test.TestFileUtils; import org.labkey.test.params.FieldDefinition; -import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.TestDataGenerator; import org.labkey.test.util.TestLogger; @@ -296,10 +295,10 @@ public static String tsvStringFromRowMaps(List> rowMaps, Lis return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.TDF); } - public static String tsvStringFromRowMapsWithQuote(List> rowMaps, List columns, - boolean includeHeaders) + public static String tsvStringFromRowMapsEscapeBackslash(List> rowMaps, List columns, + boolean includeHeaders) { - return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.MONGODB_TSV); + return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.MYSQL); } public static List> mapsFromRows(List> allRows) @@ -445,9 +444,9 @@ public static File writeRowsToTsv(String fileName, List> rows) throw return writeRowsToFile(fileName, rows, CSVFormat.TDF); } - public static File writeRowsToTsvWithQuote(String fileName, List> rows) throws IOException + public static File writeRowsToTsvEscapeBackslash(String fileName, List> rows) throws IOException { - return writeRowsToFile(fileName, rows, CSVFormat.MONGODB_TSV); + return writeRowsToFile(fileName, rows, CSVFormat.MYSQL); } From 9054850515067b6c2207969cc28d75c2a37ecff0 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 4 Aug 2025 16:48:42 -0700 Subject: [PATCH 11/12] fix windows --- src/org/labkey/test/tests/SampleTypeTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index bb4fb2354e..dcd79c78ee 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -1662,7 +1662,8 @@ public void testFilePathOnBulkImport() throws IOException importSampleTypeFilePathDataError("Fail", homeFileInfo.fileName() + "bad"); // happy cases: create new records using valid relative or absolute file in Project/Child String header = "Name\t" + fileFieldName + "\n"; - String homeSampleContent = "S-home-fullPath\t" + homeFileInfo.absoluteFilePath() + "\n" + TestDataUtils.TsvQuoter tsvQuoter = new TestDataUtils.TsvQuoter(); + String homeSampleContent = "S-home-fullPath\t" + tsvQuoter.quoteValue(homeFileInfo.absoluteFilePath()) + "\n" + "S-home-relativeDav\t" + homeFileInfo.webDavUrlRelative() + "\n" + "S-home-dataUrl\t" + homeFileInfo.dataFileUrl() + "\n" + "S-home-davUrl\t" + homeFileInfo.webDavUrl() + "\n" @@ -1688,7 +1689,7 @@ public void testFilePathOnBulkImport() throws IOException checker().verifyEqualsSorted("File field not imported as expected", List.of(fName, fName, fName, fName, fName), drt.getColumnDataAsText(fileFieldName)); importDataPage = drt.clickImportBulkData(); importDataPage.setCopyPasteMerge(false, true); - String homeSampleUpdateContent = "S-home-fullPath\t" + homeFileBInfo.absoluteFilePath() + "\n" + String homeSampleUpdateContent = "S-home-fullPath\t" + tsvQuoter.quoteValue(homeFileBInfo.absoluteFilePath()) + "\n" + "S-home-relativeDav\t\n" + "S-home-dataUrl\t" + homeFileBInfo.dataFileUrl() + "\n" + "S-home-davUrl\t" + homeFileBInfo.webDavUrl() + "\n" @@ -1736,7 +1737,7 @@ public void testFilePathOnBulkImport() throws IOException importSampleTypeFilePathDataError("Fail", subDirInfo.webDavUrlRelative()); importSampleTypeFilePathDataError("Fail", subDirInfo.fileName()); // happy case for creating child sample - String childSampleContent = "S-child-fullPath\t" + subFileInfo.absoluteFilePath() + "\n" + String childSampleContent = "S-child-fullPath\t" + tsvQuoter.quoteValue(subFileInfo.absoluteFilePath()) + "\n" + "S-child-relativeDav\t" + subFileInfo.webDavUrlRelative() + "\n" + "S-child-dataUrl\t" + subFileInfo.dataFileUrl() + "\n" + "S-child-davUrl\t" + subFileInfo.webDavUrl() + "\n" From 57e00198311c86daf4a70149902f63e7e23aa63c Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 4 Aug 2025 18:37:43 -0700 Subject: [PATCH 12/12] more code review changes, add test for LKS file availability check --- .../test/tests/AttachmentFieldTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/org/labkey/test/tests/AttachmentFieldTest.java b/src/org/labkey/test/tests/AttachmentFieldTest.java index 48e14a7046..673002985b 100644 --- a/src/org/labkey/test/tests/AttachmentFieldTest.java +++ b/src/org/labkey/test/tests/AttachmentFieldTest.java @@ -13,6 +13,7 @@ import org.labkey.test.components.DomainDesignerPage; import org.labkey.test.components.domain.DomainFieldRow; import org.labkey.test.components.domain.DomainFormPanel; +import org.labkey.test.pages.admin.FileRootsManagementPage; import org.labkey.test.pages.experiment.UpdateSampleTypePage; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.experiment.SampleTypeDefinition; @@ -81,6 +82,15 @@ public void testFileFieldInSampleType() setFormElement(Locator.name("quf_" + fieldName), SAMPLE_FILE); clickButton("Submit"); + assertElementPresent(Locator.tagWithAttribute("a", "title", "Download attached file")); + + clickAndWait(Locator.tagWithText("a", "S1")); + clickAndWait(Locator.tagWithClass("a", "labkey-text-link").withText("edit")); + waitForElement(Locator.tagContainingText("div", "sampletype/jpg_sample.jpg")); + // Issue 53200: Update form incorrectly shows that a file is not available + assertTextNotPresent("sampletype/jpg_sample.jpg (unavailable)"); + clickButton("Cancel"); + log("Verifying view in browser works"); clickAndWait(Locator.tagWithAttributeContaining("img", "title", SAMPLE_FILE.getName())); Assertions.assertThat(getDriver().getCurrentUrl()).as("File field view URL.").contains("core-downloadFileLink.view"); @@ -92,6 +102,49 @@ public void testFileFieldInSampleType() File downloadedFile = doAndWaitForDownload(() -> Locator.tagWithAttributeContaining("img", "title", SAMPLE_FILE.getName()).findElement(getDriver()).click()); Assert.assertTrue("Downloaded file is empty", downloadedFile.length() > 0); + + // create a subfolder and set the Project file root to child folder file root, to simulate sample file path not under current file root + String subFolder = "ChildFolder"; + _containerHelper.createSubfolder(getProjectName(), subFolder); + clickFolder(subFolder); + FileRootsManagementPage fileRootsManagementPage = goToFolderManagement().goToFilesTab(); + String childFileRoot = fileRootsManagementPage.getRootPath(); + goToProjectHome(); + fileRootsManagementPage = goToFolderManagement().goToFilesTab(); + fileRootsManagementPage.useCustomFileRoot(childFileRoot).clickSave(); + + // verify file path display for files that's present but outside of current file root + verifyUnavailableFile(); + + // reset file root to default + goToFolderManagement() + .goToFilesTab() + .selectFileRootType(FileRootsManagementPage.FileRootOption.siteDefault) + .clickSave(); + goToProjectHome(); + clickAndWait(Locator.linkWithText(sampleTypeName)); + assertElementPresent(Locator.tagWithAttribute("a", "title", "Download attached file")); + + // delete the file and verify file path that doesn't exist + goToModule("FileContent"); + _fileBrowserHelper.deleteFile("sampletype"); + verifyUnavailableFile(); + } + + private void verifyUnavailableFile() + { + String sampleTypeName = "Sample type with attachment"; + goToProjectHome(); + clickAndWait(Locator.linkWithText(sampleTypeName)); + waitForElement(Locator.tagContainingText("td", "jpg_sample.jpg (unavailable)")); + assertElementNotPresent(Locator.tagWithAttribute("a", "title", "Download attached file")); + + // "(unavailable)" suffix is present in update view + clickAndWait(Locator.tagWithText("a", "S1")); + clickAndWait(Locator.tagWithClass("a", "labkey-text-link").withText("edit")); + waitForElement(Locator.tagContainingText("div", "jpg_sample.jpg (unavailable)")); + assertElementNotPresent(Locator.tagWithAttributeContaining("img", "src", "/_icons/image.png")); + } @Test