diff --git a/data/fileTypes/targz_sample.tar.gz b/data/fileTypes/targz_sample.tar.gz new file mode 100644 index 0000000000..2a66d93464 Binary files /dev/null and b/data/fileTypes/targz_sample.tar.gz differ diff --git a/src/org/labkey/test/TestScrubber.java b/src/org/labkey/test/TestScrubber.java index ad2f2bf112..82f19b110a 100644 --- a/src/org/labkey/test/TestScrubber.java +++ b/src/org/labkey/test/TestScrubber.java @@ -20,6 +20,7 @@ import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.SimplePostCommand; import org.labkey.test.components.html.Checkbox; +import org.labkey.test.pages.core.admin.AllowedFileExtensionAdminPage; import org.labkey.test.pages.core.admin.BaseSettingsPage; import org.labkey.test.pages.core.admin.ConfigureFileSystemAccessPage; import org.labkey.test.pages.core.admin.LimitActiveUserPage; @@ -132,6 +133,15 @@ public void cleanSiteSettings() TestLogger.error("Failed to re-enable file Upload after test", e); } + try + { + AllowedFileExtensionAdminPage.deleteAllAllowedFileExtension(createDefaultConnection()); + } + catch (Exception e) + { + TestLogger.error("Failed to remove list of allowed file extensions.", e); + } + try { LimitActiveUserPage.resetUserLimits(connection); @@ -213,4 +223,5 @@ private void disableFileUploadSetting() throws IOException, CommandException } } } + } diff --git a/src/org/labkey/test/components/html/FileInput.java b/src/org/labkey/test/components/html/FileInput.java index b6930f0745..79143cfcd4 100644 --- a/src/org/labkey/test/components/html/FileInput.java +++ b/src/org/labkey/test/components/html/FileInput.java @@ -33,6 +33,11 @@ public void set(File file) getWrapper().setFormElement(getComponentElement(), file); } + public void clear() + { + getComponentElement().clear(); + } + @Override protected void assertElementType(WebElement el) { diff --git a/src/org/labkey/test/pages/announcements/BaseUpdatePage.java b/src/org/labkey/test/pages/announcements/BaseUpdatePage.java index a73585b9e1..5331d2e075 100644 --- a/src/org/labkey/test/pages/announcements/BaseUpdatePage.java +++ b/src/org/labkey/test/pages/announcements/BaseUpdatePage.java @@ -42,12 +42,22 @@ public PAGE setTitle(String title) return getThis(); } + public String getTitle() + { + return elementCache().titleInput.get(); + } + public PAGE setBody(String body) { elementCache().bodyInput.set(body); return getThis(); } + public String getBody() + { + return elementCache().bodyInput.get(); + } + public PAGE setRenderAs(WikiRendererType renderAs) { elementCache().rendererSelect.set(renderAs); diff --git a/src/org/labkey/test/pages/core/admin/AllowedFileExtensionAdminPage.java b/src/org/labkey/test/pages/core/admin/AllowedFileExtensionAdminPage.java new file mode 100644 index 0000000000..7c86feec6b --- /dev/null +++ b/src/org/labkey/test/pages/core/admin/AllowedFileExtensionAdminPage.java @@ -0,0 +1,219 @@ +package org.labkey.test.pages.core.admin; + +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.SimplePostCommand; +import org.labkey.test.BootstrapLocators; +import org.labkey.test.Locator; +import org.labkey.test.Locators; +import org.labkey.test.WebDriverWrapper; +import org.labkey.test.WebTestHelper; +import org.labkey.test.components.html.Input; +import org.labkey.test.pages.LabKeyPage; +import org.labkey.test.util.PortalHelper; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.StaleElementReferenceException; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AllowedFileExtensionAdminPage extends LabKeyPage +{ + public AllowedFileExtensionAdminPage(WebDriver driver) + { + super(driver); + } + + public static AllowedFileExtensionAdminPage beginAt(WebDriverWrapper webDriverWrapper) + { + webDriverWrapper.beginAt(WebTestHelper.buildURL("admin", "allowList", + Map.of("type", "FileExtension"))); + return new AllowedFileExtensionAdminPage(webDriverWrapper.getDriver()); + } + + @Override + protected void waitForPage() + { + waitFor(()-> { + try + { + return !BootstrapLocators.loadingSpinner.areAnyVisible(getDriver()) && + elementCache().extension.getComponentElement().isDisplayed(); + } + catch (NoSuchElementException | StaleElementReferenceException | TimeoutException retry) + { + return false; + } + }, "Allowed File Extensions page did not load in time.", 3_000); + + } + + /** + * Clears any file extensions that are set as the only allowable names, letting users upload any filename they like. + * + * @param connection A connection object to use in the command.execute call. + */ + public static void deleteAllAllowedFileExtension(Connection connection) throws IOException, CommandException + { + SimplePostCommand command = new SimplePostCommand("admin", "deleteAllValues"); + Map params = new HashMap<>(); + params.put("type", "FileExtension"); + command.setParameters(params); + command.execute(connection, "/"); + } + + + public AllowedFileExtensionAdminPage setExtension(String extension) + { + elementCache().extension.set(extension); + return this; + } + + public String clickSaveExpectingError() + { + return clickButtonExpectingError(elementCache().saveExtension); + } + + public AllowedFileExtensionAdminPage clickSaveExtension() + { + return clickButtonNoError(elementCache().saveExtension); + } + + public AllowedFileExtensionAdminPage updateExtension(String oldExtension, String newExtension) + { + getAllowedExtension(getAllowedExtensionIndex(oldExtension)).set(newExtension); + clearCache(); + return this; + } + + public AllowedFileExtensionAdminPage clickSaveUpdateExtension() + { + return clickButtonNoError(elementCache().saveUpdateExtension); + } + + public String clickUpdateExtensionExpectingError() + { + return clickButtonExpectingError(elementCache().saveUpdateExtension); + } + + private AllowedFileExtensionAdminPage clickButtonNoError(WebElement button) + { + clickAndWait(button); + clearCache(); + assertNoLabKeyErrors(); + return this; + } + + private String clickButtonExpectingError(WebElement button) + { + clickAndWait(button); + clearCache(); + return waitForElement(Locators.labkeyError).getText(); + } + + public AllowedFileExtensionAdminPage deleteExtension(int index) + { + WebElement deleteButton = elementCache().deleteExtensions.get(index); + deleteButton.click(); + + shortWait().withMessage("Existing extenstion was not deleted.") + .until(ExpectedConditions.stalenessOf(deleteButton)); + + clearCache(); + return this; + } + + public AllowedFileExtensionAdminPage deleteExtension(String extension) + { + return deleteExtension(getAllowedExtensionIndex(extension)); + } + + public AllowedFileExtensionAdminPage deleteAllExtensions(boolean acceptAlert) + { + if (acceptAlert) + { + doAndWaitForPageToLoad(() -> { + elementCache().deleteAll.click(); + acceptAlert(); + }); + clearCache(); + assertFalse("Delete All button should not be present after deleting all extensions", elementCache().deleteAll.isDisplayed()); + } + else + { + elementCache().deleteAll.click(); + cancelAlert(); + assertTrue("Delete All button should be present", elementCache().deleteAll.isDisplayed()); + } + return this; + } + + public List getAllowedExtensions() + { + List collection = Locator.inputByIdContaining("existingValue") + .findElements(elementCache().existingPanel); + + return collection.stream().map(el -> new Input(el, getDriver())).collect(Collectors.toList()); + + } + + public Input getAllowedExtension(int index) + { + return getAllowedExtensions().get(index); + } + + public Integer getAllowedExtensionIndex(String extension) + { + int index = 0; + List allowedExtensions = getAllowedExtensions(); + + for(Input element : allowedExtensions) + { + if (element.getValue().equalsIgnoreCase(extension)) + return index; + + index++; + } + + return -1; + } + + @Override + protected ElementCache newElementCache() + { + return new ElementCache(); + } + + protected class ElementCache extends LabKeyPage.ElementCache + { + final WebElement registerNewPanel = PortalHelper.Locators.webPart("Register New Allowed File Extension").findWhenNeeded(this); + final WebElement existingPanel = PortalHelper.Locators.webPart("Existing Allowed File Extensions").findWhenNeeded(this); + + final Input extension = new Input(Locator.id("newValueTextField").findWhenNeeded(registerNewPanel), getDriver()); + final WebElement saveExtension = Locator.lkButton("Save").findWhenNeeded(registerNewPanel); + + final WebElement saveUpdateExtension = Locator.lkButton("Save").findWhenNeeded(existingPanel); + + List allowedExtensions() + { + return Locator.inputByIdContaining("existingValue") + .findElements(elementCache().existingPanel) + .stream().map(el -> new Input(el, getDriver())).collect(Collectors.toList()); + } + + final List deleteExtensions = Locator.linkWithText("Delete").findElements(existingPanel); + + final WebElement deleteAll = Locator.linkWithText("Delete All").findWhenNeeded(existingPanel); + + } +} diff --git a/src/org/labkey/test/pages/core/admin/ShowAdminPage.java b/src/org/labkey/test/pages/core/admin/ShowAdminPage.java index 9ba38a6290..7417290227 100644 --- a/src/org/labkey/test/pages/core/admin/ShowAdminPage.java +++ b/src/org/labkey/test/pages/core/admin/ShowAdminPage.java @@ -99,6 +99,13 @@ public ShowAuditLogPage clickAuditLog() return new ShowAuditLogPage(getDriver()); } + public AllowedFileExtensionAdminPage clickAllowedFileExtensions() + { + goToSettingsSection(); + clickAndWait(elementCache().allowedFileExtensionLink); + return new AllowedFileExtensionAdminPage(getDriver()); + } + public void clickAuditLogMaintenance() { goToSettingsSection(); @@ -280,6 +287,7 @@ protected class ElementCache extends LabKeyPage.ElementCache protected WebElement analyticsSettingsLink = Locator.linkWithText("analytics settings").findWhenNeeded(this); protected WebElement externalRedirectHostLink = Locator.linkWithText("allowed external redirect hosts").findElement(this); + protected WebElement allowedFileExtensionLink = Locator.linkWithText("allowed file extensions").findElement(this); protected WebElement auditLogLink = Locator.linkWithText("audit log").findWhenNeeded(this); protected WebElement auditLogMaintenanceLink = Locator.linkWithText("Audit Log Maintenance").findWhenNeeded(this); protected WebElement authenticationLink = Locator.linkWithText("authentication").findWhenNeeded(this); diff --git a/src/org/labkey/test/tests/core/admin/AllowedFileExtensionBaseTest.java b/src/org/labkey/test/tests/core/admin/AllowedFileExtensionBaseTest.java new file mode 100644 index 0000000000..671b573e57 --- /dev/null +++ b/src/org/labkey/test/tests/core/admin/AllowedFileExtensionBaseTest.java @@ -0,0 +1,66 @@ +package org.labkey.test.tests.core.admin; + +import org.labkey.remoteapi.CommandException; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.TestFileUtils; +import org.labkey.test.components.html.Input; +import org.labkey.test.pages.core.admin.AllowedFileExtensionAdminPage; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public abstract class AllowedFileExtensionBaseTest extends BaseWebDriverTest +{ + + protected final File TAR_FILE = TestFileUtils.getSampleData("fileTypes/targz_sample.tar.gz"); + protected final File CSV_FILE = TestFileUtils.getSampleData("fileTypes/csv_sample.csv"); + protected final File TSV_FILE = TestFileUtils.getSampleData("fileTypes/tsv_sample.tsv"); + protected final File TXT_FILE = TestFileUtils.getSampleData("fileTypes/sample.txt"); + protected final File XLS_FILE = TestFileUtils.getSampleData("fileTypes/xls_sample.xls"); + protected final File XLSX_FILE = TestFileUtils.getSampleData("fileTypes/xlsx_sample.xlsx"); + + protected final Map fileMap = Map.of( + ".tar.gz", TAR_FILE, + ".xls", XLS_FILE, + ".tsv", TSV_FILE, + ".csv", CSV_FILE, + ".txt", TXT_FILE, + ".xlsx", XLSX_FILE + ); + + @Override + protected void doCleanup(boolean afterTest) + { + _containerHelper.deleteProject(getProjectName(), afterTest); + try + { + AllowedFileExtensionAdminPage.deleteAllAllowedFileExtension(createDefaultConnection()); + } + catch (IOException | CommandException e) + { + throw new RuntimeException(e); + } + } + + protected AllowedFileExtensionAdminPage setAllowedExtensions(List allowedTypes, List expectedTypes) + { + AllowedFileExtensionAdminPage allowedFileExtensionAdminPage = goToAdminConsole().clickAllowedFileExtensions(); + + for (String extension : allowedTypes) + { + allowedFileExtensionAdminPage.setExtension(extension); + allowedFileExtensionAdminPage.clickSaveExtension(); + } + + List extensions = allowedFileExtensionAdminPage.getAllowedExtensions(); + + checker().withScreenshot() + .verifyEqualsSorted("List of 'Allowed extensions' is not as expected.", + expectedTypes, extensions.stream().map(Input::getValue).toList()); + + return allowedFileExtensionAdminPage; + } + +} diff --git a/src/org/labkey/test/tests/core/admin/AllowedFileExtensionTest.java b/src/org/labkey/test/tests/core/admin/AllowedFileExtensionTest.java new file mode 100644 index 0000000000..70b95debb2 --- /dev/null +++ b/src/org/labkey/test/tests/core/admin/AllowedFileExtensionTest.java @@ -0,0 +1,525 @@ +package org.labkey.test.tests.core.admin; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.remoteapi.CommandException; + +import org.labkey.remoteapi.Connection; +import org.labkey.test.Locator; +import org.labkey.test.categories.Git; +import org.labkey.test.components.ext4.Window; +import org.labkey.test.components.html.FileInput; +import org.labkey.test.components.html.Input; +import org.labkey.test.pages.core.admin.AllowedFileExtensionAdminPage; +import org.labkey.test.params.FieldDefinition; +import org.labkey.test.params.experiment.SampleTypeDefinition; +import org.labkey.test.params.list.IntListDefinition; +import org.labkey.test.params.list.ListDefinition; +import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.Ext4Helper; +import org.labkey.test.util.PortalHelper; +import org.labkey.test.util.SampleTypeHelper; +import org.labkey.test.util.exp.SampleTypeAPIHelper; +import org.labkey.test.pages.announcements.InsertPage; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.labkey.test.util.DataRegionTable.DataRegion; + +@Category({Git.class}) +public class AllowedFileExtensionTest extends AllowedFileExtensionBaseTest +{ + + @BeforeClass + public static void setupProject() + { + AllowedFileExtensionTest init = getCurrentTest(); + init.doSetup(); + } + + private void doSetup() + { + _containerHelper.createProject(getProjectName(), null); + + goToProjectHome(); + new PortalHelper(getDriver()).doInAdminMode(ph -> { + ph.addWebPart("Files"); + ph.addWebPart("Sample Types"); + ph.addWebPart("Lists"); + ph.addWebPart("Messages List"); + }); + + } + + @Before + public void beforeTest() throws IOException, CommandException + { + log("Use API to delete any existing allowed extensions."); + AllowedFileExtensionAdminPage.deleteAllAllowedFileExtension(createDefaultConnection()); + + log("Delete any files that have already been uploaded."); + goToProjectHome(); + _fileBrowserHelper.deleteAll(); + } + + /** + *

+ * Test the 'Allowed File Extension' using the files web part as part of the validation process. This also + * validates some changes made on the admin page behave as expected. + *

+ *

+ * This test will: + *

    + *
  • Add several file extensions as allowed extensions, then upload files of that type.
  • + *
  • Upload a file that is not allowed and validate it is rejected.
  • + *
  • Remove an allowed file type and validate files of that type cannot be uploaded.
  • + *
  • Click 'Delete All' and validate any file type can be uploaded.
  • + *
  • Edit an allowed extension, .xls to .xlsx, and validate .xlsx files can be uploaded, but .xls cannot.
  • + *
+ *

+ */ + @Test + public void testAllowedFileExtensionsInFileWebPart() + { + + List allowedTypes = new ArrayList<>(); + List excludedTypes = new ArrayList<>(); + String excludedType = ".xlsx"; + excludedTypes.add(excludedType); + + for (String extension : fileMap.keySet()) + { + if (!excludedTypes.contains(extension)) + allowedTypes.add(extension); + } + + log(String.format("Add the following as allowed extensions: %s", allowedTypes)); + setAllowedExtensions(allowedTypes, allowedTypes); + + log("Verify upload of allowed file types is successful."); + uploadToFileWebPartAllowed(excludedTypes); + + log(String.format("Verify upload of '%s' fails", excludedType)); + uploadToFileWebPartExcluded(excludedTypes, allowedTypes); + + AllowedFileExtensionAdminPage allowedFileExtensionAdminPage = goToAdminConsole().clickAllowedFileExtensions(); + + allowedFileExtensionAdminPage.deleteExtension(allowedTypes.get(0)); + excludedType = allowedTypes.remove(0); + excludedTypes.add(excludedType); + + log(String.format("Remove '%s' as an allowed extension.", excludedType)); + + List extensions = allowedFileExtensionAdminPage.getAllowedExtensions(); + + checker().withScreenshot() + .verifyEqualsSorted(String.format("List of 'Allowed extensions' is not as expected after removing '%s'.", excludedType), + allowedTypes, extensions.stream().map(Input::getValue).toList()); + + log(String.format("Verify upload of '%s' fails.", excludedType)); + uploadToFileWebPartExcluded(excludedTypes, allowedTypes); + + log("Click 'Delete All' and accept the confirmation dialog."); + + allowedFileExtensionAdminPage = goToAdminConsole().clickAllowedFileExtensions(); + + allowedFileExtensionAdminPage.deleteAllExtensions(true); + + extensions = allowedFileExtensionAdminPage.getAllowedExtensions(); + + checker().withScreenshot() + .verifyTrue("List of 'Allowed extensions' is not as expected after 'Delete All'.", + extensions.isEmpty()); + + log("Validate 'all' file types can be uploaded."); + uploadToFileWebPartAllowed(new ArrayList<>()); + + allowedTypes = new ArrayList<>(); + excludedTypes = new ArrayList<>(); + excludedType = ".xlsx"; + excludedTypes.add(excludedType); + + for (String extension : fileMap.keySet()) + { + if (!excludedTypes.contains(extension)) + allowedTypes.add(extension); + } + + log(String.format("Add these extensions back: %s", allowedTypes)); + allowedFileExtensionAdminPage = setAllowedExtensions(allowedTypes, allowedTypes); + + String oldExtension = ".xls"; + String newExtension = excludedType; + + log(String.format("Edit the extension '%s' and change it to '%s'.", oldExtension, newExtension)); + + Input editExtension = allowedFileExtensionAdminPage.getAllowedExtension(allowedFileExtensionAdminPage.getAllowedExtensionIndex(oldExtension)); + + editExtension.setValue(newExtension); + + log("Save the change."); + allowedFileExtensionAdminPage.clickSaveUpdateExtension(); + + allowedTypes.remove(oldExtension); + allowedTypes.add(newExtension); + + excludedTypes = List.of(oldExtension); + extensions = allowedFileExtensionAdminPage.getAllowedExtensions(); + + checker().withScreenshot() + .verifyEqualsSorted("List of 'Allowed extensions' is not as expected.", + allowedTypes, extensions.stream().map(Input::getValue).toList()); + + uploadToFileWebPartAllowed(excludedTypes); + + log(String.format("Verify file with the old extension '%s' is excluded.", oldExtension)); + uploadToFileWebPartExcluded(excludedTypes, allowedTypes); + + } + + private void uploadToFileWebPartAllowed(List excludedTypes) + { + goToProjectHome(); + _fileBrowserHelper.deleteAll(); + + List expectedFiles = new ArrayList<>(); + for (Map.Entry entry : fileMap.entrySet()) + { + if (!excludedTypes.contains(entry.getKey())) + { + _fileBrowserHelper.uploadFile(entry.getValue()); + expectedFiles.add(entry.getValue().getName()); + } + } + + List actualFiles = _fileBrowserHelper.getFileList(); + + checker().withScreenshot() + .verifyEqualsSorted("Uploaded files not as expected.", + expectedFiles, actualFiles); + + } + + private void uploadToFileWebPartExcluded(List excludedTypes, List allowedTypes) + { + + goToProjectHome(); + + Collections.sort(allowedTypes); + + for (String excludedType : excludedTypes) + { + + if (_fileBrowserHelper.fileIsPresent(fileMap.get(excludedType).getName())) + { + log(String.format("Remove the '%s' file from the upload.", excludedType)); + _fileBrowserHelper.deleteFile(fileMap.get(excludedType).getName()); + refresh(); + } + + Window errorWindow = _fileBrowserHelper.uploadFileExpectingError(fileMap.get(excludedType)); + + checker().withScreenshot() + .verifyEquals(String.format("Error message for excluded file type '%s' not as expected.", excludedType), + String.format("This file type [%s] is not allowed. Accepted file extensions: %s", + excludedType.replace(".", ""), allowedTypes), + errorWindow.getBody()); + + click(Ext4Helper.Locators.ext4Button("OK")); + } + + } + + /** + *

+ * Using a list, validate that attachments work correctly with the allowed files list. + *

+ *

+ * After creating a list that has an auto-index and a attachment field as the only field this test will: + *

    + *
  • Can insert an element into the list with the allowed file type.
  • + *
  • Validate that a file of the type not allowed is not allowed.
  • + *
  • Remove the bad file and resubmit with a valid file type.
  • + *
+ *

+ * + */ + @Test + public void testAllowedFileExtensionsInLists() throws IOException, CommandException + { + + List allowedTypes = new ArrayList<>(); + String excludedType = ".csv"; + + for (String extension : fileMap.keySet()) + { + if (!extension.equals(excludedType)) + allowedTypes.add(extension); + } + + log(String.format("Add the following as allowed extensions: %s", allowedTypes)); + setAllowedExtensions(allowedTypes, allowedTypes); + + goToProjectHome(); + String listName = "Test Allowed Attachments"; + String attachmentField = "Attachment Field"; + + log(String.format("Create a list '%s' with attachment field and auto-increment key.", listName)); + Connection connection = createDefaultConnection(); + ListDefinition listDef = new IntListDefinition(listName, "Key"); + listDef.addField(new FieldDefinition(attachmentField, FieldDefinition.ColumnType.Attachment)); + listDef.create(connection, getProjectName()); + + goToManageLists(); + _listHelper.goToList(listName); + + String [][] expectedData = new String [allowedTypes.size()][1]; + int index = 0; + for (String allowedType : allowedTypes) + { + _listHelper.insertNewRow(Map.of(attachmentField, fileMap.get(allowedType).getAbsolutePath()), false); + // Add a space before the name to allow for the icon. + expectedData[index++][0] = String.format(" %s", fileMap.get(allowedType).getName()); + } + + goToManageLists(); + _listHelper.goToList(listName); + _listHelper.verifyListData(List.of(new FieldDefinition(attachmentField, FieldDefinition.ColumnType.Attachment)), expectedData, checker()); + + _listHelper.goToList(listName); + _listHelper.insertNewRow(Map.of(attachmentField, fileMap.get(excludedType).getAbsolutePath()), false); + + validateErrorPage(fileMap.get(excludedType).getName(), allowedTypes); + + log("Click 'Back' button and select a file type that is allowed."); + waitForElement(Locator.button("Back")); + clickButton("Back"); + + waitForElement(Locator.name("quf_" + attachmentField)); + + // Issue 53026, the fields are cleared after hitting the back button. Unlikely the issue will be fixed. + log("Clear the file field."); + FileInput el = FileInput.FileInput(Locator.name("quf_" + attachmentField), getDriver()).findWhenNeeded(); + el.clear(); + + File fileAgain = fileMap.get(".txt"); + log(String.format("Add the '%s' file to the list again (it is an allowed file).", fileAgain.getName())); + el.set(fileAgain.getAbsolutePath()); + + clickButton("Submit"); + + DataRegionTable dataRegion = DataRegion(getDriver()).withName("query").find(); + List actualData = dataRegion.getColumnDataAsText(attachmentField); + + checker().withScreenshot() + .verifyEquals(String.format("The file '%s' should be in the list twice.", fileAgain.getName()), + 2, Collections.frequency(actualData, String.format(" %s", fileAgain.getName()))); + + } + + /** + *

+ * Validate sample types work well with the allowed extension list. + *

+ *

+ * This test will set several extension as allowed type, create a sample type with a file field, and then: + *

    + *
  • Validate a row(s) can be inserted with the allowed file types.
  • + *
  • Validate a row cannot be inserted with a disallowed file type.
  • + *
  • Validate that removing the disallowed type from the file field allows for the row to be inserted.
  • + *
+ * Note: This test does not exercise bulk or import by file. In those scenarios the file would have had + * to already been uploaded to the server for it to be referenced in the input or bulk file / data. Uploading to + * the server is covered in the testAddUpdateAndDelete test. + *

+ */ + @Test + public void testAllowedFileExtensionsInSampleType() + { + List allowedTypes = new ArrayList<>(); + List excludedTypes = new ArrayList<>(); + String excludedType = ".xlsx"; + excludedTypes.add(excludedType); + + for (String extension : fileMap.keySet()) + { + if (!excludedTypes.contains(extension)) + allowedTypes.add(extension); + } + + log(String.format("Add the following as allowed extensions: %s", allowedTypes)); + setAllowedExtensions(allowedTypes, allowedTypes); + + goToProjectHome(); + + String stName = "Allowed File Extension Testing"; + SampleTypeDefinition stDefinition = new SampleTypeDefinition(stName); + + String stFileField = "File Upload Test"; + stDefinition.addField(new FieldDefinition(stFileField, FieldDefinition.ColumnType.File)); + SampleTypeAPIHelper.createEmptySampleType(getProjectName(), stDefinition); + + refresh(); + waitForElement(Locator.linkWithText(stName)); + + SampleTypeHelper sampleTypeHelper = new SampleTypeHelper(this); + sampleTypeHelper.goToSampleType(stName); + + log("Add rows in the sample type with the allowed file types."); + int i = 1; + Map fieldMap; + List expectedValues = new ArrayList<>(); + for (String allowedType : allowedTypes) + { + fieldMap = Map.of("Name", String.format("S-%d", i), stFileField, fileMap.get(allowedType).getAbsolutePath()); + sampleTypeHelper.insertRow(fieldMap); + expectedValues.add(String.format(" sampletype/%s", fileMap.get(allowedType).getName())); + i++; + } + + // The order of the grid is the last one added is at the top, which is opposite of how they were added to the list. + Collections.reverse(expectedValues); + + List actualValues = sampleTypeHelper.getSamplesDataRegionTable().getColumnDataAsText(stFileField); + checker().verifyEquals(String.format("Values in the '%s' column not as expected.", stFileField), + expectedValues, actualValues); + + log("Create a sample that tries to upload a disallowed file type."); + String sampleId = String.format("S-%d", i); + String description= "Some text for the description."; + String amount = "5.0"; + + fieldMap = Map.of("Name", sampleId, + "Description", description, + stFileField, fileMap.get(excludedType).getAbsolutePath(), + "StoredAmount", amount); + sampleTypeHelper.insertRow(fieldMap); + + validateErrorPage(fileMap.get(excludedType).getName(), allowedTypes); + + // Issue 53027 + goToProjectHome(); + sampleTypeHelper = new SampleTypeHelper(this); + sampleTypeHelper.goToSampleType(stName); + fieldMap = Map.of("Name", sampleId, + "Description", description, + "StoredAmount", amount); + sampleTypeHelper.insertRow(fieldMap); + + Map rowMap = sampleTypeHelper.getSamplesDataRegionTable().getRowDataAsMap(0); + + checker().verifyEquals("'Name' field in grid does not have expected value.", + sampleId, rowMap.get("Name")); + + checker().verifyEquals("'Amount' field in grid does not have expected value.", + amount, rowMap.get("StoredAmount")); + + checker().verifyEquals(String.format("'%s' field in grid does not have expected value.", stFileField), + " ", rowMap.get(stFileField)); + + checker().screenShotIfNewError("Field_Values_Error"); + + } + + /** + *

+ * Validate that message attachments work correctly with the allowed files list. + *

+ *

+ * This test will: + *

    + *
  • Create a message with several allowed files as attachments.
  • + *
  • Verify a message created with a disallowed file type is not allowed.
  • + *
  • Remove the disallowed file from the list of attachments and resubmit.
  • + *
+ *

+ */ + @Test + public void testAllowedFilesInMessages() + { + + List allowedTypes = new ArrayList<>(); + String excludedType = ".tsv"; + + for (String extension : fileMap.keySet()) + { + if (!extension.equals(excludedType)) + allowedTypes.add(extension); + } + + log(String.format("Add the following as allowed extensions: %s", allowedTypes)); + setAllowedExtensions(allowedTypes, allowedTypes); + + goToProjectHome(); + + String allowedTitle = "Allowed Files Attachment"; + + InsertPage page = InsertPage.beginAt(this) + .setTitle(allowedTitle) + .setBody("These attachments should be allowed."); + + for (String allowedType : allowedTypes) + { + page.addAttachments(fileMap.get(allowedType)); + } + + log(String.format("Create a message with title of '%s' and several allowed files as attachments.", allowedTitle)); + + page.submit(); + + String notAllowedTitle = "Not Allowed Files Attachment"; + String notAllowedBody = "At least one of these attachments should not be allowed."; + + page = InsertPage.beginAt(this) + .setTitle(notAllowedTitle) + .setBody(notAllowedBody); + + for (Map.Entry entry : fileMap.entrySet()) + { + page.addAttachments(entry.getValue()); + } + + log(String.format("Try to create a message with title of '%s' with several allowed files as attachments, and one disallowed file type.", allowedTitle)); + + page.submit(); + + validateErrorPage(fileMap.get(excludedType).getName(), allowedTypes); + + } + + private void validateErrorPage(String notAllowedFile, List allowedTypes) + { + + Collections.sort(allowedTypes); + String notAllowedFileExtension = notAllowedFile.substring(notAllowedFile.lastIndexOf(".") + 1); + + String expectedErrorMsg = String.format("%s: This file type [%s] is not allowed. Accepted file extensions: %s", + notAllowedFile, notAllowedFileExtension, allowedTypes); + + checker().withScreenshot() + .verifyTrue(String.format("Error message '%s' not found on the error page.", expectedErrorMsg), + waitForElement(Locator.tagContainingText("div", expectedErrorMsg).withClass("labkey-error-heading"), + 1_500, false)); + } + + @Override + protected String getProjectName() + { + return "Allowed File Extension Test Project " + TRICKY_CHARACTERS_FOR_PROJECT_NAMES; + } + + @Override + public List getAssociatedModules() + { + return Arrays.asList(); + } + +} diff --git a/src/org/labkey/test/tests/core/admin/AllowedFileExtensionsPageTest.java b/src/org/labkey/test/tests/core/admin/AllowedFileExtensionsPageTest.java new file mode 100644 index 0000000000..b98d1537aa --- /dev/null +++ b/src/org/labkey/test/tests/core/admin/AllowedFileExtensionsPageTest.java @@ -0,0 +1,166 @@ +package org.labkey.test.tests.core.admin; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.remoteapi.CommandException; +import org.labkey.test.Locator; +import org.labkey.test.categories.Git; +import org.labkey.test.components.html.Input; +import org.labkey.test.pages.core.admin.AllowedFileExtensionAdminPage; +import org.openqa.selenium.Alert; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Category({Git.class}) +public class AllowedFileExtensionsPageTest extends AllowedFileExtensionBaseTest +{ + @BeforeClass + public static void setupProject() + { + AllowedFileExtensionsPageTest init = getCurrentTest(); + init.doSetup(); + } + + private void doSetup() + { + _containerHelper.createProject(getProjectName(), null); + goToProjectHome(); + } + + @Before + public void beforeTest() throws IOException, CommandException + { + log("Use API to delete any existing allowed extensions."); + AllowedFileExtensionAdminPage.deleteAllAllowedFileExtension(createDefaultConnection()); + } + + /** + *

+ * Test the 'Allowed File Extension Admin'. + *

+ *

+ * This test will: + *

    + *
  • Add several file extensions as allowed extensions, and validate they are listed in alphabetical order.
  • + *
  • Click 'Delete All' and cancel out of confirmation.
  • + *
  • Validate extension value must start with a '.'
  • + *
  • Validate duplicate extensions are not allowed.
  • + *
  • Validate blank extension type is not allowed.
  • + *
+ *

+ */ + @Test + public void testAllowFileExtensionsAdminPage() + { + + List allowedTypes = new ArrayList<>(fileMap.keySet()); + + log(String.format("Add the following as allowed extensions: %s", allowedTypes)); + AllowedFileExtensionAdminPage allowedFileExtensionAdminPage = setAllowedExtensions(allowedTypes, allowedTypes); + + log("Validate that the allowed file extensions are listed in alphabetical order."); + List actualValues = allowedFileExtensionAdminPage.getAllowedExtensions().stream().map(Input::getValue).toList(); + Collections.sort(allowedTypes); + + checker().withScreenshot() + .verifyEquals("List of 'Allowed extensions' is not in the expected order.", + allowedTypes, actualValues); + + log("Click 'Delete All' but cancel out of the confirmation dialog."); + + allowedFileExtensionAdminPage.deleteAllExtensions(false); + + List extensions = allowedFileExtensionAdminPage.getAllowedExtensions(); + + checker().withScreenshot() + .verifyEqualsSorted("List of 'Allowed extensions' is not as expected after canceling 'Delete All'.", + allowedTypes, extensions.stream().map(Input::getValue).toList()); + + // Issue 38785 + // Selenium launches the firefox browser with a preference set that (basically) disabled the dirty bit. + // As a result, checking for the dirty bit dialog prompt, or a missing dialog, is not doable until issue 38785 + // is addressed. + // Issue 53039. +// log("Validate that canceling the 'Delete All' dialog does not set the dirty bit."); +// +// // Using goToProjectHome will validate no dirty bit is set and navigation can happen. +// goToProjectHome(); +// +// allowedFileExtensionAdminPage = goToAdminConsole().clickAllowedFileExtensions(); + + // Issue 38785 + // Selenium launches the firefox browser with a prefernce set that (basically) disabled the dirty bit. + // As a result, checking for the dirty bit dialog prompt, or a missing dialog, is doable until 38785 is addressed. +// String oldExtension = ".txt"; +// String newExtension = ".pdf"; +// +// log(String.format("Edit the extension '%s' and change it to '%s'.", oldExtension, newExtension)); +// +// Input editExtension = allowedFileExtensionAdminPage.getAllowedExtension(allowedFileExtensionAdminPage.getAllowedExtensionIndex(oldExtension)); +// +// editExtension.setValue(newExtension); +// +// // Issue 53039 validate dirty bit warning. +// log("Validate that an alert is shown if the change is not saved."); +// Locator.tagWithClass("a", "brand-logo").findElement(getDriver()).click(); +// Alert alert = waitForAlert(); +// +// checker().withScreenshot() +// .verifyTrue("Alert text doesn't have expected text.", +// alert.getText().contains("Changes you made may not be saved.")); +// +// log("Dismiss the alert."); +// alert.dismiss(); +// +// log("Save the change."); +// allowedFileExtensionAdminPage.clickSaveUpdateExtension(); + + allowedFileExtensionAdminPage.setExtension("not .an extension"); + String expectedValue = "File extension must start with a '.'"; + String actualValue = allowedFileExtensionAdminPage.clickSaveExpectingError(); + checker().withScreenshot() + .verifyEquals("Incorrect error message for invalid extension.", + expectedValue, actualValue); + + allowedFileExtensionAdminPage.setExtension(allowedTypes.get(0)); + expectedValue = String.format("'%s' already exists. Duplicate values not allowed.", allowedTypes.get(0)); + actualValue = allowedFileExtensionAdminPage.clickSaveExpectingError(); + checker().withScreenshot() + .verifyEquals("Incorrect error message for duplicate extension.", + expectedValue, actualValue); + + allowedFileExtensionAdminPage.setExtension(allowedTypes.get(1).toUpperCase()); + expectedValue = String.format("'%s' already exists. Duplicate values not allowed.", allowedTypes.get(1).toUpperCase()); + actualValue = allowedFileExtensionAdminPage.clickSaveExpectingError(); + checker().withScreenshot() + .verifyEquals("Incorrect error message for duplicate extension but different case.", + expectedValue, actualValue); + + allowedFileExtensionAdminPage.setExtension(""); + expectedValue = "File extension must not be blank."; + actualValue = allowedFileExtensionAdminPage.clickSaveExpectingError(); + checker().withScreenshot() + .verifyEquals("Incorrect error message for blank extension value.", + expectedValue, actualValue); + + } + + @Override + protected String getProjectName() + { + return "Allowed File Extension Page Test Project " + TRICKY_CHARACTERS_FOR_PROJECT_NAMES; + } + + @Override + public List getAssociatedModules() + { + return Arrays.asList(); + } + +} diff --git a/src/org/labkey/test/util/FileBrowserHelper.java b/src/org/labkey/test/util/FileBrowserHelper.java index 98837af563..c01e1746b8 100644 --- a/src/org/labkey/test/util/FileBrowserHelper.java +++ b/src/org/labkey/test/util/FileBrowserHelper.java @@ -550,14 +550,7 @@ public void uploadFile(@LoggedParam final File file, @Nullable String descriptio { int initialCount = waitForFileGridReady(); - openUploadPanel(); - - waitFor(() -> getFormElement(Locator.xpath("//label[text() = 'Choose a File:']/../..//input[contains(@class, 'x4-form-field')]")).isEmpty(), - "Upload field did not clear after upload.", WAIT_FOR_JAVASCRIPT); - - setFormElement(Locator.css(".single-upload-panel input:last-of-type[type=file]"), file); - waitFor(() -> getFormElement(Locator.xpath("//label[text() = 'Choose a File:']/../..//input[contains(@class, 'x4-form-field')]")).contains(file.getName()), - "Upload field was not set to '" + file.getName() + "'.", WAIT_FOR_JAVASCRIPT); + setChooseAFile(file); if (description != null) setFormElement(Locator.name("description"), description); @@ -602,6 +595,25 @@ public void uploadFile(@LoggedParam final File file, @Nullable String descriptio assertEquals("Description didn't clear after upload", "", getFormElement(Locator.name("description"))); } + @LogMethod + public Window uploadFileExpectingError(File file) + { + waitForFileGridReady(); + setChooseAFile(file); + clickButton("Upload", WAIT_FOR_EXT_MASK_TO_DISSAPEAR); + return new Window.WindowFinder(getDriver()).withTitle("Error").waitFor(); + } + + private void setChooseAFile(File file) + { + openUploadPanel(); + waitFor(() -> getFormElement(Locator.xpath("//label[text() = 'Choose a File:']/../..//input[contains(@class, 'x4-form-field')]")).isEmpty(), + "Upload field did not clear after upload.", WAIT_FOR_JAVASCRIPT); + setFormElement(Locator.css(".single-upload-panel input:last-of-type[type=file]"), file); + waitFor(() -> getFormElement(Locator.xpath("//label[text() = 'Choose a File:']/../..//input[contains(@class, 'x4-form-field')]")).contains(file.getName()), + "Upload field was not set to '" + file.getName() + "'.", WAIT_FOR_JAVASCRIPT); + } + private void dragAndDropFileInDropZone(File file) { //Offsets for the drop zone