Skip to content

Commit 9974733

Browse files
committed
Making it harder to delete all rows from lists
1 parent af4fa5f commit 9974733

1 file changed

Lines changed: 388 additions & 0 deletions

File tree

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
package org.labkey.test.tests.list;
2+
3+
import org.apache.hc.core5.http.HttpStatus;
4+
import org.junit.Assert;
5+
import org.junit.BeforeClass;
6+
import org.junit.Test;
7+
import org.junit.experimental.categories.Category;
8+
import org.labkey.api.collections.CaseInsensitiveHashMap;
9+
import org.labkey.remoteapi.CommandException;
10+
import org.labkey.test.BaseWebDriverTest;
11+
import org.labkey.test.Locator;
12+
import org.labkey.test.TestFileUtils;
13+
import org.labkey.test.WebTestHelper;
14+
import org.labkey.test.categories.Daily;
15+
import org.labkey.test.categories.Data;
16+
import org.labkey.test.categories.Hosting;
17+
import org.labkey.test.components.list.ManageListsGrid;
18+
import org.labkey.test.pages.list.BeginPage;
19+
import org.labkey.test.util.DataRegionTable;
20+
import org.labkey.test.params.FieldDefinition;
21+
import org.labkey.test.params.list.IntListDefinition;
22+
import org.labkey.test.params.list.VarListDefinition;
23+
import org.labkey.test.util.DomainUtils;
24+
import org.labkey.test.util.TestDataGenerator;
25+
import org.labkey.test.util.TestUser;
26+
import org.openqa.selenium.By;
27+
import org.openqa.selenium.support.ui.ExpectedConditions;
28+
29+
import java.io.File;
30+
import java.io.IOException;
31+
import java.util.List;
32+
33+
import static org.junit.Assert.assertEquals;
34+
import static org.junit.Assert.assertTrue;
35+
import static org.labkey.test.util.PermissionsHelper.EDITOR_ROLE;
36+
37+
@Category({Daily.class, Data.class, Hosting.class})
38+
@BaseWebDriverTest.ClassTimeout(minutes = 5)
39+
public class ListDeleteTest extends BaseWebDriverTest
40+
{
41+
private static final String PROJECT_NAME = "ListDeleteTest";
42+
private static final String PROJECT_PATH = "/" + PROJECT_NAME;
43+
private static final String SUBFOLDER_A_NAME = "SubfolderA";
44+
private static final String SUBFOLDER_A_PATH = PROJECT_PATH + "/" + SUBFOLDER_A_NAME;
45+
private static final String SUBFOLDER_B_NAME = "SubfolderB";
46+
private static final String SUBFOLDER_B_PATH = PROJECT_PATH + "/" + SUBFOLDER_B_NAME;
47+
48+
private static final TestUser LIST_DESIGNER_USER = new TestUser("listdesigner@listdelete.test");
49+
50+
private static final String attachmentFieldName = TestDataGenerator.randomFieldName("Attachment", null, DomainUtils.DomainKind.IntList);
51+
private static final String booleanFieldName = TestDataGenerator.randomFieldName("Boolean", null, DomainUtils.DomainKind.IntList);
52+
private static final String integerFieldName = TestDataGenerator.randomFieldName("Integer", null, DomainUtils.DomainKind.IntList);
53+
private static final String stringFieldName = TestDataGenerator.randomFieldName("String", null, DomainUtils.DomainKind.IntList);
54+
55+
private static final String autoIncrementKeyFieldName1 = TestDataGenerator.randomFieldName("Key", null, DomainUtils.DomainKind.IntList);
56+
57+
protected static final File IMG_FILE = TestFileUtils.getSampleData("InlineImages/help.jpg"); // use in Project
58+
protected static final File PDF_FILE = TestFileUtils.getSampleData("InlineImages/agraph.pdf"); // use in Subfolder A
59+
protected static final File TXT_FILE = TestFileUtils.getSampleData("InlineImages/test.txt"); // use in Subfolder B
60+
61+
private static IntListDefinition LIST_1; // int list with attachment column
62+
private static VarListDefinition LIST_2; // var list with attachment column, keyed by string
63+
64+
@BeforeClass
65+
public static void setupProject() throws Exception
66+
{
67+
ListDeleteTest init = getCurrentTest();
68+
init.doSetup();
69+
}
70+
71+
private void doSetup() throws Exception
72+
{
73+
// Create project and subfolders
74+
_containerHelper.createProject(getProjectName(), null);
75+
_containerHelper.createSubfolder(getProjectName(), SUBFOLDER_A_NAME);
76+
_containerHelper.createSubfolder(getProjectName(), SUBFOLDER_B_NAME);
77+
78+
// Create user with Assay Designer + Editor permissions
79+
LIST_DESIGNER_USER.create(this)
80+
.setInitialPassword()
81+
.addPermission(EDITOR_ROLE, PROJECT_PATH)
82+
.addPermission("Assay Designer", PROJECT_PATH)
83+
.addPermission(EDITOR_ROLE, SUBFOLDER_A_PATH)
84+
.addPermission("Assay Designer", SUBFOLDER_A_PATH)
85+
.addPermission(EDITOR_ROLE, SUBFOLDER_B_PATH);
86+
87+
var conn = createDefaultConnection();
88+
89+
// Create list 1 with attachment column
90+
var list1Name = DomainUtils.DomainKind.IntList.randomName("DEL1");
91+
LIST_1 = (IntListDefinition) new IntListDefinition(list1Name, autoIncrementKeyFieldName1)
92+
.setFields(List.of(
93+
new FieldDefinition(attachmentFieldName, FieldDefinition.ColumnType.Attachment),
94+
new FieldDefinition(booleanFieldName, FieldDefinition.ColumnType.Boolean),
95+
new FieldDefinition(integerFieldName, FieldDefinition.ColumnType.Integer),
96+
new FieldDefinition(stringFieldName, FieldDefinition.ColumnType.String)
97+
));
98+
LIST_1.getCreateCommand().execute(conn, PROJECT_PATH);
99+
100+
// Create list 2 — var list with attachment column, keyed by string
101+
var stringKeyField = new FieldDefinition(stringFieldName);
102+
var list2Name = DomainUtils.DomainKind.VarList.randomName("DEL2");
103+
LIST_2 = (VarListDefinition) new VarListDefinition(list2Name)
104+
.setKeyName(stringKeyField.getName())
105+
.setFields(List.of(
106+
stringKeyField,
107+
new FieldDefinition(attachmentFieldName, FieldDefinition.ColumnType.Attachment),
108+
new FieldDefinition(booleanFieldName, FieldDefinition.ColumnType.Boolean),
109+
new FieldDefinition(integerFieldName, FieldDefinition.ColumnType.Integer)
110+
));
111+
LIST_2.getCreateCommand().execute(conn, PROJECT_PATH);
112+
113+
// Populate data in project folder for both lists
114+
populateList1(PROJECT_PATH, IMG_FILE);
115+
populateList2(PROJECT_PATH, IMG_FILE);
116+
117+
// Populate data in subfolder A for both lists
118+
populateList1(SUBFOLDER_A_PATH, PDF_FILE);
119+
populateList2(SUBFOLDER_A_PATH, PDF_FILE);
120+
121+
// Populate data in subfolder B for both lists
122+
populateList1(SUBFOLDER_B_PATH, TXT_FILE);
123+
populateList2(SUBFOLDER_B_PATH, TXT_FILE);
124+
}
125+
126+
private void populateList1(String containerPath, File attachment) throws IOException, CommandException
127+
{
128+
var dataGenerator = LIST_1.getTestDataGenerator(containerPath)
129+
.addDataSupplier(attachmentFieldName, () -> attachment);
130+
131+
// Insert rows with attachment values via UI (only way to provide attachment values)
132+
var attachmentRows = dataGenerator.withGeneratedRows(1)
133+
.getRows();
134+
135+
_listHelper.beginAtList(containerPath, LIST_1.getName());
136+
var newRow = new CaseInsensitiveHashMap<>();
137+
newRow.putAll(attachmentRows.getFirst());
138+
_listHelper.insertNewRow(newRow, false);
139+
}
140+
141+
private void populateList2(String containerPath, File attachment) throws IOException, CommandException
142+
{
143+
var dataGenerator = LIST_2.getTestDataGenerator(containerPath)
144+
.addDataSupplier(stringFieldName, () -> "String" + containerPath)
145+
.addDataSupplier(attachmentFieldName, () -> attachment);
146+
147+
// Insert rows without attachments via API
148+
var attachmentRows = dataGenerator.withGeneratedRows(1)
149+
.getRows();
150+
151+
_listHelper.beginAtList(containerPath, LIST_2.getName());
152+
var newRow = new CaseInsensitiveHashMap<>();
153+
newRow.putAll(attachmentRows.getFirst());
154+
_listHelper.insertNewRow(newRow, false);
155+
}
156+
157+
private void verifyConfirmationPage(String containerPath, List<String> listNames)
158+
{
159+
// Navigate to manage lists page, clear all row selections, verify delete button is disabled
160+
var listsPage = BeginPage.beginAt(this, containerPath);
161+
var grid = listsPage.getGrid();
162+
grid.uncheckAllOnPage();
163+
var deleteButton = grid.getHeaderButton("Delete");
164+
Assert.assertTrue("Delete button should be disabled when no rows selected",
165+
deleteButton.getAttribute("class").contains("disabled"));
166+
167+
// Select lists, verify delete menu is enabled and has 2 options
168+
selectLists(grid, listNames);
169+
var menuOptions = grid.getHeaderMenuOptions("Delete");
170+
Assert.assertEquals("Expected 2 delete menu options",
171+
List.of("Delete List", "Delete All Data from List"), menuOptions);
172+
173+
// Click DELETE -> "Delete List", verify landing on confirmation page with expected text, cancel
174+
listsPage = BeginPage.beginAt(this, containerPath);
175+
grid = listsPage.getGrid();
176+
selectLists(grid, listNames);
177+
grid.clickHeaderMenu("Delete", true, "Delete List");
178+
assertTextPresent("Are you sure you want to delete the following Lists?");
179+
for (String listName : listNames)
180+
assertElementPresent(Locator.linkWithText(listName));
181+
clickButton("Cancel");
182+
183+
// Click DELETE -> "Delete All Data from List", verify landing on confirmation page with expected text, cancel
184+
listsPage = BeginPage.beginAt(this, containerPath);
185+
grid = listsPage.getGrid();
186+
selectLists(grid, listNames);
187+
grid.clickHeaderMenu("Delete", true, "Delete All Data from List");
188+
assertTextPresent("Are you sure you want to delete all data");
189+
assertTextPresent("This action cannot be undone and will result in an empty list.");
190+
for (String listName : listNames)
191+
{
192+
assertElementPresent(Locator.linkWithText(listName));
193+
}
194+
assertTextPresent("1 row");
195+
clickButton("Cancel");
196+
}
197+
198+
private void selectLists(ManageListsGrid grid, List<String> listNames)
199+
{
200+
for (String listName : listNames)
201+
grid.checkCheckbox(grid.getRowIndex("Name", listName));
202+
}
203+
204+
private void verifyListRowCount(String containerPath, String listName, int expectedCount)
205+
{
206+
_listHelper.beginAtList(containerPath, listName);
207+
var table = new DataRegionTable("query", getDriver());
208+
Assert.assertEquals("Expected " + expectedCount + " rows in " + listName + " at " + containerPath,
209+
expectedCount, table.getDataRowCount());
210+
}
211+
212+
private void verifyListDataWithAttachment(String containerPath, String listName, int expectedCount, String attachmentFileName)
213+
{
214+
_listHelper.beginAtList(containerPath, listName);
215+
var table = new DataRegionTable("query", getDriver());
216+
Assert.assertEquals("Expected " + expectedCount + " rows in " + listName + " at " + containerPath,
217+
expectedCount, table.getDataRowCount());
218+
219+
if (attachmentFileName.contains(IMG_FILE.getName()))
220+
{
221+
log("Hover over the thumbnail for the image and make sure the pop-up is as expected.");
222+
// Mouse over the logo, migh help with the following mouse over the image.
223+
mouseOver(Locator.tagWithAttributeContaining("img", "src", IMG_FILE.getName()));
224+
sleep(500);
225+
mouseOver(Locator.xpath("//img[contains(@title, '" + IMG_FILE.getName() + "')]"));
226+
longWait().until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#helpDiv")));
227+
String src = Locator.xpath("//div[@id='helpDiv']//img[contains(@src, 'download')]").findElement(getDriver()).getAttribute("src");
228+
assertTrue("Wrong image in popup: " + src, src.contains(IMG_FILE.getName()));
229+
assertEquals("Bad response from image pop-up", HttpStatus.SC_OK, WebTestHelper.getHttpResponse(src).getResponseCode());
230+
231+
}
232+
else
233+
{
234+
File download = doAndWaitForDownload(() -> click(Locator.linkWithText(attachmentFileName)));
235+
Assert.assertTrue("Downloaded attachment should exist: " + attachmentFileName, download.exists());
236+
}
237+
238+
}
239+
240+
/**
241+
* Verifies list deletion and data truncation across folders and permission levels.
242+
*
243+
* <ol>
244+
* <li>Verify "Delete List" and "Delete All Data from List" confirmation pages render correctly
245+
* for single and multi-list selections across project and subfolders.</li>
246+
* <li>Impersonate a non-admin designer user (Editor + Assay Designer) and verify they see a
247+
* simple "Delete" button (no "Delete All Data" menu option) and can reach the delete
248+
* confirmation page. Cancel without deleting.</li>
249+
* <li>As admin, truncate all list data from Subfolder A. Verify both lists are empty in
250+
* Subfolder A while data and attachments remain intact in the project and Subfolder B.</li>
251+
* <li>As admin, truncate only LIST_2 data from the project. Verify LIST_2 is empty in the
252+
* project while LIST_1 data in the project and all data in Subfolder B are unaffected.</li>
253+
* <li>Impersonate the designer user again. Verify no Delete button appears in Subfolder B
254+
* (where the user lacks Assay Designer permission). Delete LIST_1 from Subfolder A and
255+
* LIST_2 from the project, verifying the list definitions are removed.</li>
256+
* </ol>
257+
*/
258+
@Test
259+
public void testDeleteListData() throws IOException, CommandException
260+
{
261+
verifyConfirmationPage(getProjectName(), List.of(LIST_1.getName()));
262+
verifyConfirmationPage(getProjectName(), List.of(LIST_1.getName(), LIST_2.getName()));
263+
verifyConfirmationPage(SUBFOLDER_A_PATH, List.of(LIST_1.getName()));
264+
verifyConfirmationPage(SUBFOLDER_B_PATH, List.of(LIST_1.getName(), LIST_2.getName()));
265+
266+
LIST_DESIGNER_USER.impersonate();
267+
268+
// verify DESIGNER don't see the menu option to "Delete All Data from List", only "Delete" button
269+
var listsPage = BeginPage.beginAt(this, getProjectName());
270+
var grid = listsPage.getGrid();
271+
grid.uncheckAllOnPage();
272+
var deleteButton = grid.getHeaderButton("Delete");
273+
Assert.assertTrue("Delete button should be disabled when no rows selected",
274+
deleteButton.getAttribute("class").contains("disabled"));
275+
276+
// Select lists, verify delete menu is enabled and has 2 options
277+
selectLists(grid, List.of(LIST_1.getName(), LIST_2.getName()));
278+
// verify DELETE button is enabled, verify click DELETE land on confirmation page, click cancel
279+
Assert.assertFalse("Delete button should be enabled when rows are selected",
280+
deleteButton.getAttribute("class").contains("disabled"));
281+
grid.clickHeaderButton("Delete");
282+
assertTextPresent("Are you sure you want to delete the following Lists?");
283+
for (String listName : List.of(LIST_1.getName(), LIST_2.getName()))
284+
assertElementPresent(Locator.linkWithText(listName));
285+
clickButton("Cancel");
286+
stopImpersonating();
287+
288+
// Verify deleting data from Subfolder A doesn't impact lists or data in project folder or Subfolder B
289+
listsPage = BeginPage.beginAt(this, SUBFOLDER_A_PATH);
290+
grid = listsPage.getGrid();
291+
selectLists(grid, List.of(LIST_1.getName(), LIST_2.getName()));
292+
grid.clickHeaderMenu("Delete", true, "Delete All Data from List");
293+
assertTextPresent("Are you sure you want to delete all data");
294+
assertTextPresent("This action cannot be undone and will result in an empty list.");
295+
for (String listName : List.of(LIST_1.getName(), LIST_2.getName()))
296+
{
297+
assertElementPresent(Locator.linkWithText(listName));
298+
assertTextPresent("1 row");
299+
}
300+
clickButton("Confirm Delete All Data");
301+
302+
// Verify data deleted in Subfolder A — both lists should be empty
303+
verifyListRowCount(SUBFOLDER_A_PATH, LIST_1.getName(), 0);
304+
verifyListRowCount(SUBFOLDER_A_PATH, LIST_2.getName(), 0);
305+
306+
// Verify data still exists in project folder
307+
// Go to LIST_1, verify grid is not empty, verify attachment can still be downloaded successfully
308+
verifyListDataWithAttachment(PROJECT_PATH, LIST_1.getName(), 1, IMG_FILE.getName());
309+
310+
// Go to Subfolder B, go to LIST_2, verify data present, verify attachment can still be downloaded successfully
311+
verifyListDataWithAttachment(SUBFOLDER_B_PATH, LIST_2.getName(), 1, TXT_FILE.getName());
312+
313+
// Now delete just LIST_2 data from project folder
314+
listsPage = BeginPage.beginAt(this, PROJECT_PATH);
315+
grid = listsPage.getGrid();
316+
selectLists(grid, List.of(LIST_2.getName()));
317+
grid.clickHeaderMenu("Delete", true, "Delete All Data from List");
318+
319+
// Verify confirmation page
320+
assertTextPresent("Are you sure you want to delete all data");
321+
assertElementPresent(Locator.linkWithText(LIST_2.getName()));
322+
assertElementNotPresent(Locator.linkWithText(LIST_1.getName()));
323+
assertTextPresent("1 row");
324+
clickButton("Confirm Delete All Data");
325+
326+
// Verify data deleted from LIST_2 in project
327+
verifyListRowCount(PROJECT_PATH, LIST_2.getName(), 0);
328+
329+
// Verify data still present in LIST_2 in Subfolder B and in LIST_1 in both folders
330+
verifyListRowCount(SUBFOLDER_B_PATH, LIST_2.getName(), 1);
331+
verifyListRowCount(PROJECT_PATH, LIST_1.getName(), 1);
332+
verifyListRowCount(SUBFOLDER_B_PATH, LIST_1.getName(), 1);
333+
334+
LIST_DESIGNER_USER.impersonate();
335+
336+
// From Subfolder B, verify LIST_DESIGNER_USER cannot delete LIST_1 or LIST_2
337+
// since they don't have designer permission in the sub folder
338+
listsPage = BeginPage.beginAt(this, SUBFOLDER_B_PATH);
339+
grid = listsPage.getGrid();
340+
Assert.assertFalse("Delete button should not be present without designer permission",
341+
grid.hasHeaderMenu("Delete"));
342+
343+
// From Subfolder A, verify LIST_DESIGNER_USER can delete LIST_1
344+
listsPage = BeginPage.beginAt(this, SUBFOLDER_A_PATH);
345+
grid = listsPage.getGrid();
346+
selectLists(grid, List.of(LIST_1.getName()));
347+
grid.clickHeaderButtonAndWait("Delete");
348+
assertTextPresent("Are you sure you want to delete the following Lists?");
349+
assertElementPresent(Locator.linkWithText(LIST_1.getName()));
350+
clickButton("Confirm Delete");
351+
352+
// Verify LIST_1 is deleted successfully
353+
listsPage = BeginPage.beginAt(this, PROJECT_PATH);
354+
grid = listsPage.getGrid();
355+
Assert.assertFalse("LIST_1 should no longer exist",
356+
grid.getListNames().contains(LIST_1.getName()));
357+
Assert.assertTrue("LIST_2 should still exist",
358+
grid.getListNames().contains(LIST_2.getName()));
359+
360+
// From project folder, verify LIST_DESIGNER_USER can delete LIST_2
361+
selectLists(grid, List.of(LIST_2.getName()));
362+
grid.clickHeaderButtonAndWait("Delete");
363+
assertTextPresent("Are you sure you want to delete the following Lists?");
364+
assertElementPresent(Locator.linkWithText(LIST_2.getName()));
365+
clickButton("Confirm Delete");
366+
367+
stopImpersonating();
368+
}
369+
370+
@Override
371+
protected void doCleanup(boolean afterTest)
372+
{
373+
super.doCleanup(afterTest);
374+
_userHelper.deleteUsers(afterTest, LIST_DESIGNER_USER);
375+
}
376+
377+
@Override
378+
protected String getProjectName()
379+
{
380+
return PROJECT_NAME;
381+
}
382+
383+
@Override
384+
public List<String> getAssociatedModules()
385+
{
386+
return List.of("list");
387+
}
388+
}

0 commit comments

Comments
 (0)