diff --git a/src/org/labkey/test/AssayAPITest.java b/src/org/labkey/test/AssayAPITest.java index c01c17812b..a3f1945a62 100644 --- a/src/org/labkey/test/AssayAPITest.java +++ b/src/org/labkey/test/AssayAPITest.java @@ -15,9 +15,11 @@ */ package org.labkey.test; +import org.jetbrains.annotations.Nullable; 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 +33,13 @@ 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; import org.labkey.test.util.UIAssayHelper; import java.io.File; @@ -47,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; @@ -228,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") @@ -247,6 +257,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"); @@ -267,28 +280,56 @@ 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); 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 +337,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"); @@ -303,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()); + ((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")); @@ -322,23 +367,83 @@ 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"); + 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"); + ((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() + "')]")); + // 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()); 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); + + // 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) + "\": \"" + EscapeUtil.toJSONStr(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/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 diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index 88b1b6547f..dcd79c78ee 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -51,14 +51,18 @@ 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; 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; @@ -1582,58 +1586,187 @@ public void testFileAttachment() @Test // Issue 49830 public void testFilePathOnBulkImport() throws IOException { - projectMenu().navigateToFolder(PROJECT_NAME, FOLDER_NAME); + new ApiPermissionsHelper(this) + .setSiteRoleUserPermissions(PasswordUtil.getUsername(), "See Absolute File Paths"); + + 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); - private DataRegionTable importSampleTypeFilePathData(String sampleTypeName, String fileFieldName, String sampleName, String filePath) - { - projectMenu().navigateToFolder(PROJECT_NAME, FOLDER_NAME); - clickAndWait(Locator.linkWithText(sampleTypeName)); + goToProjectHome(); + clickAndWait(Locator.linkWithText(sampleTypeNameHome)); DataRegionTable drt = DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); - drt.clickImportBulkData(); + var 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 data = sampleName + "\t" + filePath + "\n"; - setFormElement(Locator.name("text"), header + data); + 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" + + "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"); - return DataRegionTable.findDataRegionWithinWebpart(this, "Sample Type Contents"); + 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" + tsvQuoter.quoteValue(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" + 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" + + "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 void importSampleTypeFilePathDataError(String sampleName, String filePath) + { + final String fileFieldName = "FileField"; + 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(); + 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/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 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; 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) diff --git a/src/org/labkey/test/util/FileBrowserHelper.java b/src/org/labkey/test/util/FileBrowserHelper.java index c01e1746b8..b4ac3b149e 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,58 @@ 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", "FileExists", "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); + + 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) + { + if (ce.getStatusCode() == 404) + { + return null; + } + else + { + throw new RuntimeException(ce); + } + } + catch (IOException ioe) + { + throw new RuntimeException(ioe); + } + + return null; + } + // See PageFlowUtil.encodeURIComponent() private static final Map DECODE_UNRESERVED_MARKS = Map.of( "!", "%21", diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index 8c0a71d50c..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,6 +295,12 @@ public static String tsvStringFromRowMaps(List> rowMaps, Lis return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.TDF); } + public static String tsvStringFromRowMapsEscapeBackslash(List> rowMaps, List columns, + boolean includeHeaders) + { + return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.MYSQL); + } + public static List> mapsFromRows(List> allRows) { List> rowMaps = new ArrayList<>(); @@ -439,6 +444,12 @@ public static File writeRowsToTsv(String fileName, List> rows) throw return writeRowsToFile(fileName, rows, CSVFormat.TDF); } + public static File writeRowsToTsvEscapeBackslash(String fileName, List> rows) throws IOException + { + return writeRowsToFile(fileName, rows, CSVFormat.MYSQL); + } + + public static File writeRowsToCsv(String fileName, List> rows) throws IOException { return writeRowsToFile(fileName, rows, CSVFormat.DEFAULT);