Skip to content

Commit 6699ff6

Browse files
Merge 26.3 to 26.7
2 parents e82420f + 94cbe52 commit 6699ff6

1 file changed

Lines changed: 226 additions & 2 deletions

File tree

‎src/org/labkey/test/tests/AttachmentFieldTest.java‎

Lines changed: 226 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,23 @@
1919
import org.assertj.core.api.Assertions;
2020
import org.jetbrains.annotations.Nullable;
2121
import org.junit.Assert;
22+
import org.junit.Assume;
2223
import org.junit.Before;
2324
import org.junit.BeforeClass;
2425
import org.junit.Test;
2526
import org.junit.experimental.categories.Category;
27+
import org.labkey.remoteapi.CommandException;
28+
import org.labkey.remoteapi.assay.Batch;
29+
import org.labkey.remoteapi.assay.Data;
30+
import org.labkey.remoteapi.assay.Run;
31+
import org.labkey.remoteapi.assay.SaveAssayBatchCommand;
32+
import org.labkey.remoteapi.query.Filter;
33+
import org.labkey.remoteapi.query.InsertRowsCommand;
34+
import org.labkey.remoteapi.query.SelectRowsCommand;
2635
import org.labkey.test.BaseWebDriverTest;
2736
import org.labkey.test.Locator;
2837
import org.labkey.test.TestFileUtils;
38+
import org.labkey.test.TestProperties;
2939
import org.labkey.test.WebTestHelper;
3040
import org.labkey.test.categories.Daily;
3141
import org.labkey.test.pages.ReactAssayDesignerPage;
@@ -34,27 +44,35 @@
3444
import org.labkey.test.pages.list.EditListDefinitionPage;
3545
import org.labkey.test.params.FieldDefinition;
3646
import org.labkey.test.params.experiment.SampleTypeDefinition;
47+
import org.labkey.test.params.list.IntListDefinition;
3748
import org.labkey.test.util.ApiPermissionsHelper;
3849
import org.labkey.test.util.DataRegionTable;
3950
import org.labkey.test.util.PasswordUtil;
4051
import org.labkey.test.util.PermissionsHelper;
4152
import org.labkey.test.util.PortalHelper;
4253
import org.labkey.test.util.SampleTypeHelper;
54+
import org.labkey.test.util.SimpleHttpResponse;
4355
import org.labkey.test.util.TestDataGenerator;
44-
4556
import org.openqa.selenium.By;
4657
import org.openqa.selenium.support.ui.ExpectedConditions;
4758

4859
import java.io.File;
60+
import java.io.IOException;
4961
import java.net.URI;
62+
import java.nio.file.Files;
63+
import java.nio.file.StandardCopyOption;
5064
import java.util.List;
65+
import java.util.Map;
66+
import java.util.regex.Matcher;
67+
import java.util.regex.Pattern;
5168

5269
@Category({Daily.class})
53-
@BaseWebDriverTest.ClassTimeout(minutes = 2)
70+
@BaseWebDriverTest.ClassTimeout(minutes = 5)
5471
public class AttachmentFieldTest extends BaseWebDriverTest
5572
{
5673
private static final String RESTRICTED_PROJECT = "AttachmentFieldTest Restricted Project";
5774
private static final String RESTRICTED_USER = "restrictedreader@attachmentfieldtest.test";
75+
private static final String OUTSIDE_FILE_CONTENT = "AttachmentFieldTest content outside every file root";
5876
private final File SAMPLE_FILE = new File(TestFileUtils.getSampleData("fileTypes"), "jpg_sample.jpg");
5977

6078
@BeforeClass
@@ -285,4 +303,210 @@ public void testDownloadFileLinkCrossContainerPermission()
285303
Assert.assertEquals("Expected 403 Forbidden when user lacks read permission on the object's container",
286304
HttpStatus.SC_FORBIDDEN, status);
287305
}
306+
307+
// GH Issue 1463
308+
@Test
309+
public void testDownloadFileLinkWithDisabledFileRoot() throws Exception
310+
{
311+
Assume.assumeFalse("Test seeds a server-side path from the local file system", TestProperties.isServerRemote());
312+
313+
final String folderName = "DisabledFileRoot";
314+
final String folderPath = getProjectName() + "/" + folderName;
315+
final String sampleTypeName = "FileLinkSamples";
316+
final String textListName = "TextPathList";
317+
final String fieldName = "LinkedFile";
318+
_containerHelper.createSubfolder(getProjectName(), folderName);
319+
320+
log("Create a sample type with a file field and a list with a same-named text field");
321+
new SampleTypeDefinition(sampleTypeName)
322+
.setFields(List.of(new FieldDefinition(fieldName, FieldDefinition.ColumnType.File)))
323+
.create(createDefaultConnection(), folderPath);
324+
new IntListDefinition(textListName, "Key")
325+
.setFields(List.of(new FieldDefinition(fieldName, FieldDefinition.ColumnType.String)))
326+
.create(createDefaultConnection(), folderPath);
327+
328+
log("Upload a file while the file root is enabled");
329+
SampleTypeHelper.beginAtSampleTypesList(this, folderPath).goToSampleType(sampleTypeName);
330+
DataRegionTable.DataRegion(getDriver()).withName("Material")
331+
.waitFor()
332+
.clickInsertNewRow()
333+
.setField("Name", "S1")
334+
.setField(fieldName, SAMPLE_FILE)
335+
.submit();
336+
String managedFileUrl = Locator.tagWithAttributeContaining("a", "href", "downloadFileLink")
337+
.findElement(getDriver()).getAttribute("href");
338+
Matcher propertyIdMatcher = Pattern.compile("propertyId=(\\d+)").matcher(managedFileUrl);
339+
Assert.assertTrue("No propertyId in download URL: " + managedFileUrl, propertyIdMatcher.find());
340+
341+
String propertyId = propertyIdMatcher.group(1);
342+
343+
log("Link a sample to a file under a custom pipeline root that is later replaced");
344+
clickFolder(folderName);
345+
File oldPipelineRoot = TestFileUtils.ensureTestTempDir(getClass().getSimpleName(), "oldPipelineRoot");
346+
File orphanedFile = new File(oldPipelineRoot, SAMPLE_FILE.getName());
347+
Files.copy(SAMPLE_FILE.toPath(), orphanedFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
348+
setPipelineRoot(oldPipelineRoot.getAbsolutePath(), false);
349+
InsertRowsCommand sampleInsert = new InsertRowsCommand("samples", sampleTypeName);
350+
sampleInsert.addRow(Map.of("Name", "S2", fieldName, orphanedFile.getAbsolutePath()));
351+
Object orphanedRowId = sampleInsert.execute(createDefaultConnection(), folderPath).getRows().getFirst().get("RowId");
352+
String outsideFileUrl = WebTestHelper.buildURL("core", folderPath, "downloadFileLink", Map.of(
353+
"propertyId", propertyId,
354+
"schemaName", "samples",
355+
"queryName", sampleTypeName,
356+
"pk", String.valueOf(orphanedRowId)));
357+
358+
log("Pair the file field's propertyId with a same-named text column in a list");
359+
InsertRowsCommand listInsert = new InsertRowsCommand("lists", textListName);
360+
listInsert.addRow(Map.of(fieldName, orphanedFile.getAbsolutePath()));
361+
Object textRowKey = listInsert.execute(createDefaultConnection(), folderPath).getRows().getFirst().get("Key");
362+
String mismatchedColumnUrl = WebTestHelper.buildURL("core", folderPath, "downloadFileLink", Map.of(
363+
"propertyId", propertyId,
364+
"schemaName", "lists",
365+
"queryName", textListName,
366+
"pk", String.valueOf(textRowKey)));
367+
368+
log("Replace the pipeline root and disable the file root, so the assay files root is unresolvable");
369+
setPipelineRoot(TestFileUtils.ensureTestTempDir(getClass().getSimpleName(), "newPipelineRoot").getAbsolutePath(), false);
370+
FileRootsManagementPage.beginAt(this, folderPath)
371+
.selectFileRootType(FileRootsManagementPage.FileRootOption.disable)
372+
.clickSave();
373+
374+
Assert.assertEquals("File under the disabled file root should still download",
375+
HttpStatus.SC_OK, WebTestHelper.getHttpResponse(managedFileUrl).getResponseCode());
376+
377+
SimpleHttpResponse outsideResponse = WebTestHelper.getHttpResponse(outsideFileUrl);
378+
Assert.assertEquals("File outside every file root should be rejected",
379+
HttpStatus.SC_NOT_FOUND, outsideResponse.getResponseCode());
380+
Assertions.assertThat(outsideResponse.getResponseBody()).as("Rejection reason")
381+
.contains("under a file root for container");
382+
383+
Assert.assertEquals("propertyId for a different column should be rejected",
384+
HttpStatus.SC_BAD_REQUEST, WebTestHelper.getHttpResponse(mismatchedColumnUrl).getResponseCode());
385+
386+
log("Revert to the default pipeline root, which is unresolvable while the file root is disabled");
387+
clickFolder(folderName);
388+
setPipelineRootToDefault();
389+
390+
Assert.assertEquals("File under the disabled file root should download without a pipeline root",
391+
HttpStatus.SC_OK, WebTestHelper.getHttpResponse(managedFileUrl).getResponseCode());
392+
Assert.assertEquals("File outside every file root should be rejected without a pipeline root",
393+
HttpStatus.SC_NOT_FOUND, WebTestHelper.getHttpResponse(outsideFileUrl).getResponseCode());
394+
}
395+
396+
@Test
397+
public void testExpDataAbsolutePathWithDisabledFileRoot() throws Exception
398+
{
399+
Assume.assumeFalse("Test seeds a server-side path from the local file system", TestProperties.isServerRemote());
400+
401+
final String folderName = "ExpDataAbsolutePath";
402+
final String folderPath = getProjectName() + "/" + folderName;
403+
_containerHelper.createSubfolder(getProjectName(), folderName);
404+
405+
log("Create a derivation run while the default pipeline root is available");
406+
Data seedData = new Data();
407+
seedData.setName("seed");
408+
seedData.setPipelinePath("/");
409+
Batch savedBatch = saveDerivationRun(folderPath, null, null, seedData);
410+
Run savedRun = savedBatch.getRuns().getFirst();
411+
412+
log("Disable the file root, so the folder has no pipeline root");
413+
FileRootsManagementPage.beginAt(this, folderPath)
414+
.selectFileRootType(FileRootsManagementPage.FileRootOption.disable)
415+
.clickSave();
416+
417+
log("Attach a file outside every file root to the existing run");
418+
File outsideFile = writeOutsideFile(TestFileUtils.ensureTestTempDir(getClass().getSimpleName(), "outsideRoots"));
419+
Data outsideData = new Data();
420+
outsideData.setName(outsideFile.getName());
421+
outsideData.setAbsolutePath(outsideFile.getAbsolutePath());
422+
try
423+
{
424+
saveDerivationRun(folderPath, savedBatch.getId(), savedRun.getId(), outsideData);
425+
}
426+
catch (CommandException expected)
427+
{
428+
Assertions.assertThat(expected.getMessage()).as("Save rejection reason")
429+
.contains("is not under the pipeline root");
430+
return;
431+
}
432+
433+
SimpleHttpResponse showFileResponse = getShowFileResponse(folderPath, getDataRowId(folderPath, outsideFile.getName()));
434+
Assert.fail("saveAssayBatch accepted a file outside every file root, and showFile then returned "
435+
+ showFileResponse.getResponseCode()
436+
+ (showFileResponse.getResponseBody().contains(OUTSIDE_FILE_CONTENT) ? " with the file's contents" : ""));
437+
}
438+
439+
@Test
440+
public void testShowFileWithDisabledFileRoot() throws Exception
441+
{
442+
Assume.assumeFalse("Test seeds a server-side path from the local file system", TestProperties.isServerRemote());
443+
444+
final String folderName = "OrphanedExpData";
445+
final String folderPath = getProjectName() + "/" + folderName;
446+
_containerHelper.createSubfolder(getProjectName(), folderName);
447+
448+
log("Create an exp.data file under a custom pipeline root that is later removed");
449+
File oldPipelineRoot = TestFileUtils.ensureTestTempDir(getClass().getSimpleName(), "oldExpDataPipelineRoot");
450+
File orphanedFile = writeOutsideFile(oldPipelineRoot);
451+
clickFolder(folderName);
452+
setPipelineRoot(oldPipelineRoot.getAbsolutePath(), false);
453+
Data orphanedData = new Data();
454+
orphanedData.setName(orphanedFile.getName());
455+
orphanedData.setAbsolutePath(orphanedFile.getAbsolutePath());
456+
saveDerivationRun(folderPath, null, null, orphanedData);
457+
int dataRowId = getDataRowId(folderPath, orphanedFile.getName());
458+
459+
log("Revert to the default pipeline root");
460+
clickFolder(folderName);
461+
setPipelineRootToDefault();
462+
Assert.assertEquals("File outside the pipeline root should be rejected",
463+
HttpStatus.SC_FORBIDDEN, getShowFileResponse(folderPath, dataRowId).getResponseCode());
464+
465+
log("Disable the file root, so the folder has no pipeline root");
466+
FileRootsManagementPage.beginAt(this, folderPath)
467+
.selectFileRootType(FileRootsManagementPage.FileRootOption.disable)
468+
.clickSave();
469+
Assert.assertEquals("File outside every file root should be rejected without a pipeline root",
470+
HttpStatus.SC_FORBIDDEN, getShowFileResponse(folderPath, dataRowId).getResponseCode());
471+
}
472+
473+
private File writeOutsideFile(File dir) throws IOException
474+
{
475+
File file = new File(dir, "outsideRoots.txt");
476+
Files.writeString(file.toPath(), OUTSIDE_FILE_CONTENT);
477+
return file;
478+
}
479+
480+
private Batch saveDerivationRun(String folderPath, @Nullable Integer batchId, @Nullable Integer runId, Data dataInput) throws IOException, CommandException
481+
{
482+
Run run = new Run();
483+
run.setName("Derivation run");
484+
if (runId != null)
485+
run.setId(runId);
486+
run.setDataInputs(List.of(dataInput));
487+
488+
Batch batch = new Batch();
489+
if (batchId != null)
490+
batch.setId(batchId);
491+
batch.getRuns().add(run);
492+
493+
return new SaveAssayBatchCommand(SaveAssayBatchCommand.SAMPLE_DERIVATION_PROTOCOL, batch)
494+
.execute(createDefaultConnection(), folderPath).getBatch();
495+
}
496+
497+
private int getDataRowId(String folderPath, String dataName) throws IOException, CommandException
498+
{
499+
SelectRowsCommand select = new SelectRowsCommand("exp", "Data");
500+
select.setColumns(List.of("RowId"));
501+
select.addFilter("Name", dataName, Filter.Operator.EQUAL);
502+
List<Map<String, Object>> rows = select.execute(createDefaultConnection(), folderPath).getRows();
503+
Assert.assertEquals("exp.data rows named " + dataName, 1, rows.size());
504+
return ((Number) rows.getFirst().get("RowId")).intValue();
505+
}
506+
507+
private SimpleHttpResponse getShowFileResponse(String folderPath, int dataRowId)
508+
{
509+
return WebTestHelper.getHttpResponse(WebTestHelper.buildURL("experiment", folderPath, "showFile",
510+
Map.of("rowId", String.valueOf(dataRowId))));
511+
}
288512
}

0 commit comments

Comments
 (0)