From b18b05ba55e68b298ebdebca44c436aa83449435 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 2 Oct 2025 13:00:09 -0700 Subject: [PATCH 1/4] ListMoveRowsTest --- .../test/params/list/ListDefinition.java | 11 + .../test/tests/list/ListMoveRowsTest.java | 508 ++++++++++++++++++ src/org/labkey/test/tests/list/ListTest.java | 8 - .../test/util/query/MoveRowsResponse.java | 80 +++ .../test/util/query/QueryApiHelper.java | 28 +- 5 files changed, 613 insertions(+), 22 deletions(-) create mode 100644 src/org/labkey/test/tests/list/ListMoveRowsTest.java create mode 100644 src/org/labkey/test/util/query/MoveRowsResponse.java diff --git a/src/org/labkey/test/params/list/ListDefinition.java b/src/org/labkey/test/params/list/ListDefinition.java index 4c1345b2f1..a1b50a158c 100644 --- a/src/org/labkey/test/params/list/ListDefinition.java +++ b/src/org/labkey/test/params/list/ListDefinition.java @@ -13,6 +13,7 @@ public abstract class ListDefinition extends DomainProps { + private Integer _listId; private String _name; private String _description; private List _fields = new ArrayList<>(); @@ -25,6 +26,16 @@ public ListDefinition(String name) _name = name; } + public Integer getListId() + { + return _listId; + } + + public void setListId(Integer listId) + { + _listId = listId; + } + public String getName() { return _name; diff --git a/src/org/labkey/test/tests/list/ListMoveRowsTest.java b/src/org/labkey/test/tests/list/ListMoveRowsTest.java new file mode 100644 index 0000000000..c3e88127f5 --- /dev/null +++ b/src/org/labkey/test/tests/list/ListMoveRowsTest.java @@ -0,0 +1,508 @@ +package org.labkey.test.tests.list; + +import org.assertj.core.api.Assertions; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.query.ContainerFilter; +import org.labkey.remoteapi.query.Filter; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.remoteapi.query.SelectRowsResponse; +import org.labkey.remoteapi.query.TruncateTableCommand; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.TestFileUtils; +import org.labkey.test.params.FieldDefinition; +import org.labkey.test.params.list.IntListDefinition; +import org.labkey.test.params.list.ListDefinition; +import org.labkey.test.params.list.VarListDefinition; +import org.labkey.test.util.AuditLogHelper; +import org.labkey.test.util.DomainUtils; +import org.labkey.test.util.TestDataGenerator; +import org.labkey.test.util.TestUser; +import org.labkey.test.util.query.MoveRowsResponse; +import org.labkey.test.util.query.QueryApiHelper; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.labkey.test.util.AuditLogHelper.AuditEvent.LIST_AUDIT_EVENT; +import static org.labkey.test.util.PermissionsHelper.AUTHOR_ROLE; +import static org.labkey.test.util.PermissionsHelper.EDITOR_ROLE; +import static org.labkey.test.util.PermissionsHelper.READER_ROLE; + +@Category({}) +public class ListMoveRowsTest extends BaseWebDriverTest +{ + private static final String PROJECT_NAME = "ListMoveRowsTest"; + private static final String PROJECT_PATH = "/" + PROJECT_NAME; + private static final String SUBFOLDER_A_NAME = "SubfolderA"; + private static final String SUBFOLDER_A_PATH = "/" + PROJECT_NAME + "/" + SUBFOLDER_A_NAME; + private static final String SUBFOLDER_MINOR_A_NAME = "MinorA"; + private static final String SUBFOLDER_MINOR_A_PATH = SUBFOLDER_A_PATH + "/" + SUBFOLDER_MINOR_A_NAME; + private static final String SUBFOLDER_B_NAME = "SubfolderB"; + private static final String SUBFOLDER_B_PATH = "/" + PROJECT_NAME + "/" + SUBFOLDER_B_NAME; + private static final String LIST_SCHEMA = "lists"; + + private static final TestUser AUTHOR_USER = new TestUser("author@listmoverows.test"); + private static final TestUser READER_USER = new TestUser("reader@listmoverows.test"); + private static final TestUser SUBFOLDER_EDITOR_USER = new TestUser("sub.editor@listmoverows.test"); + + private static final String attachmentFieldName = TestDataGenerator.randomFieldName("Attachment", null, DomainUtils.DomainKind.IntList); + private static final String booleanFieldName = TestDataGenerator.randomFieldName("Boolean", null, DomainUtils.DomainKind.IntList); + private static final String dateFieldName = TestDataGenerator.randomFieldName("Date", null, DomainUtils.DomainKind.IntList); + private static final String dateTimeFieldName = TestDataGenerator.randomFieldName("DateTime", null, DomainUtils.DomainKind.IntList); + private static final String decimalFieldName = TestDataGenerator.randomFieldName("Decimal", null, DomainUtils.DomainKind.IntList); + private static final String integerFieldName = TestDataGenerator.randomFieldName("Integer", null, DomainUtils.DomainKind.IntList); + + private static final String autoIncrementKeyFieldName = TestDataGenerator.randomFieldName("AutoIncrementKey", null, DomainUtils.DomainKind.IntList); + private static final String integerKeyFieldName = TestDataGenerator.randomFieldName("IntegerKey", null, DomainUtils.DomainKind.IntList); + private static final String stringKeyFieldName = TestDataGenerator.randomFieldName("StringKey", null, DomainUtils.DomainKind.VarList); + + private static final File SAMPLE_DATA_FILE = TestFileUtils.getSampleData("lists/ListImportFields.txt"); + + private static IntListDefinition AUTO_INCREMENT_LIST; + private static IntListDefinition INTEGER_KEY_LIST; + private static VarListDefinition STRING_KEY_LIST; + + private record UpdateCounts(int fileAttachmentsMoved, int listAuditEventsCreated, int listAuditEventsMoved, int listRecords, int queryAuditEventsMoved) { } + private static final UpdateCounts NO_UPDATE = new UpdateCounts(0, 0, 0, 0, 0); + + private final AuditLogHelper _auditLogHelper = new AuditLogHelper(this); + + @BeforeClass + public static void setupProject() throws Exception + { + ListMoveRowsTest init = getCurrentTest(); + init.doSetup(); + } + + private void doSetup() throws Exception + { + // Create folders + { + _containerHelper.createProject(getProjectName(), null); + _containerHelper.createSubfolder(getProjectName(), SUBFOLDER_A_NAME); + _containerHelper.createSubfolder(getProjectName(), SUBFOLDER_B_NAME); + _containerHelper.createSubfolder(SUBFOLDER_A_PATH, SUBFOLDER_MINOR_A_NAME); + } + + // Configure users + { + AUTHOR_USER.create(this) + .setInitialPassword() + .addPermission(AUTHOR_ROLE, PROJECT_PATH) + .addPermission(AUTHOR_ROLE, SUBFOLDER_A_PATH) + .addPermission(AUTHOR_ROLE, SUBFOLDER_B_PATH) + .addPermission(AUTHOR_ROLE, SUBFOLDER_MINOR_A_PATH); + READER_USER.create(this) + .setInitialPassword() + .addPermission(READER_ROLE, PROJECT_PATH); + SUBFOLDER_EDITOR_USER.create(this) + .setInitialPassword() + .addPermission(READER_ROLE, PROJECT_PATH) + .addPermission(EDITOR_ROLE, SUBFOLDER_A_PATH) + .addPermission(EDITOR_ROLE, SUBFOLDER_B_PATH) + .addPermission(EDITOR_ROLE, SUBFOLDER_MINOR_A_PATH); + } + + // Create lists + { + var conn = createDefaultConnection(); + var fields = getFields(); + + // List keyed by auto-increment + var autoKeyListName = DomainUtils.DomainKind.IntList.randomName("AUTO"); + AUTO_INCREMENT_LIST = (IntListDefinition) new IntListDefinition(autoKeyListName, autoIncrementKeyFieldName) + .setFields(fields); + AUTO_INCREMENT_LIST.getCreateCommand().execute(conn, PROJECT_PATH); + AUTO_INCREMENT_LIST.setListId(resolveListId(conn, autoKeyListName, PROJECT_PATH)); + + // List keyed by integer (not auto-increment) + var intKeyField = new FieldDefinition(integerKeyFieldName, FieldDefinition.ColumnType.Integer); + var intListFields = new ArrayList(); + intListFields.add(intKeyField); + intListFields.addAll(fields); + var intKeyListName = DomainUtils.DomainKind.IntList.randomName("INT"); + INTEGER_KEY_LIST = (IntListDefinition) new IntListDefinition(intKeyListName) + .setKeyName(intKeyField.getName()) + .setFields(intListFields); + INTEGER_KEY_LIST.getCreateCommand().execute(conn, PROJECT_PATH); + INTEGER_KEY_LIST.setListId(resolveListId(conn, intKeyListName, PROJECT_PATH)); + + // List keyed by string + var stringKeyField = new FieldDefinition(stringKeyFieldName); + var varListFields = new ArrayList(); + varListFields.add(stringKeyField); + varListFields.addAll(fields); + var stringKeyListName = DomainUtils.DomainKind.VarList.randomName("STR"); + STRING_KEY_LIST = (VarListDefinition) new VarListDefinition(stringKeyListName) + .setFields(varListFields) + .setKeyName(stringKeyField.getName()); + STRING_KEY_LIST.getCreateCommand().execute(conn, PROJECT_PATH); + STRING_KEY_LIST.setListId(resolveListId(conn, stringKeyListName, PROJECT_PATH)); + } + } + + @Override + protected void doCleanup(boolean afterTest) + { + super.doCleanup(afterTest); + _userHelper.deleteUsers(afterTest, AUTHOR_USER, READER_USER, SUBFOLDER_EDITOR_USER); + } + + @Test + public void testAutoIntListMove() throws Exception + { + // Arrange + truncateList(AUTO_INCREMENT_LIST); + + int totalRows = 100; + int numRowsWithAttachmentValue = 5; + var response = addRows(AUTO_INCREMENT_LIST, getProjectName(), totalRows, numRowsWithAttachmentValue); + + successfullyMoveRows(AUTO_INCREMENT_LIST, response.getRows()); + } + + @Test + public void testIntListMove() throws Exception + { + // Arrange + truncateList(INTEGER_KEY_LIST); + + int totalRows = 100; + int numRowsWithAttachmentValue = 5; + var response = addRows(INTEGER_KEY_LIST, getProjectName(), totalRows, numRowsWithAttachmentValue); + + successfullyMoveRows(INTEGER_KEY_LIST, response.getRows()); + } + + @Test + public void testVarListMove() throws Exception + { + // Arrange + truncateList(STRING_KEY_LIST); + + int totalRows = 100; + int numRowsWithAttachmentValue = 5; + var response = addRows(STRING_KEY_LIST, getProjectName(), totalRows, numRowsWithAttachmentValue); + + successfullyMoveRows(STRING_KEY_LIST, response.getRows()); + } + + @Test + public void testPermissions() throws Exception + { + truncateList(AUTO_INCREMENT_LIST); + var projectResponse = addRows(AUTO_INCREMENT_LIST, PROJECT_PATH, 10, 0); + var subfolderRows = 10; + var subfolderFileAttachments = 2; + var subfolderResponse = addRows(AUTO_INCREMENT_LIST, SUBFOLDER_A_PATH, subfolderRows, subfolderFileAttachments); + + // Verify author permissions + { + impersonate(AUTHOR_USER.getEmail()); + + // Author should not be able to move rows from subfolder to project + String expectedError = "User does not have permission to perform this operation."; + moveRowsExpectingError(AUTO_INCREMENT_LIST, SUBFOLDER_A_PATH, PROJECT_PATH, subfolderResponse.getRows(), expectedError); + + // Author should not be able to move rows from a project to a subfolder + moveRowsExpectingError(AUTO_INCREMENT_LIST, PROJECT_PATH, SUBFOLDER_A_PATH, projectResponse.getRows(), expectedError); + + stopImpersonating(); + } + + // Verify reader permissions + { + impersonate(READER_USER.getEmail()); + + // Reader should not be able to move rows from project to subfolder + String expectedError = String.format("You do not have permission to move rows from '%s' to the target container: %s.", AUTO_INCREMENT_LIST.getName(), SUBFOLDER_A_PATH); + moveRowsExpectingError(AUTO_INCREMENT_LIST, PROJECT_PATH, SUBFOLDER_A_PATH, projectResponse.getRows(), expectedError); + + // Reader should not be able to move rows from subfolder to project + expectedError = "User does not have permission to perform this operation."; + moveRowsExpectingError(AUTO_INCREMENT_LIST, SUBFOLDER_A_PATH, PROJECT_PATH, subfolderResponse.getRows(), expectedError); + + stopImpersonating(); + } + + // Verify editor permissions + { + impersonate(SUBFOLDER_EDITOR_USER.getEmail()); + + // Subfolder editor should not be able to move rows from subfolder to project where they can only read + String expectedError = String.format("You do not have permission to move rows from '%s' to the target container: %s.", AUTO_INCREMENT_LIST.getName(), PROJECT_PATH); + moveRowsExpectingError(AUTO_INCREMENT_LIST, SUBFOLDER_A_PATH, PROJECT_PATH, subfolderResponse.getRows(), expectedError); + + // Subfolder editor should not be able to move rows from a project where they can only read to a subfolder + expectedError = "User does not have permission to perform this operation."; + moveRowsExpectingError(AUTO_INCREMENT_LIST, PROJECT_PATH, SUBFOLDER_A_PATH, projectResponse.getRows(), expectedError); + + // Subfolder editor should be able to move rows from subfolder to subfolder + MoveRowsResponse moveResponse = moveRows(AUTO_INCREMENT_LIST, SUBFOLDER_A_PATH, SUBFOLDER_B_PATH, subfolderResponse.getRows()); + + stopImpersonating(); + + // We have to navigate to the project to ensure requests are made from the correct folder context. + // This is due to the audit log helper hardcoding what project it filters the audit logs by. + goToProjectHome(); + + var expectedCounts = new UpdateCounts(subfolderFileAttachments, subfolderRows + 2, subfolderRows, subfolderRows, subfolderRows - subfolderFileAttachments); + verifySuccessfulMove(AUTO_INCREMENT_LIST, moveResponse, SUBFOLDER_A_PATH, SUBFOLDER_B_PATH, expectedCounts); + } + } + + @Test + public void testListInSubfolder() throws Exception + { + // Create a list domain in a subfolder + var subfolderListName = DomainUtils.DomainKind.IntList.randomName("SUBFOLDER"); + var subfolderList = (IntListDefinition) new IntListDefinition(subfolderListName, autoIncrementKeyFieldName) + .setFields(getFields()); + var conn = createDefaultConnection(); + subfolderList.getCreateCommand().execute(conn, SUBFOLDER_A_PATH); + subfolderList.setListId(resolveListId(conn, subfolderListName, SUBFOLDER_A_PATH)); + + var subARows = addRows(subfolderList, SUBFOLDER_A_PATH, 10, 0); + + // Cannot move rows from a list defined in a subfolder to the project + String expectedError = String.format("List '%s' is not accessible from folder %s.", subfolderList.getName(), PROJECT_PATH); + moveRowsExpectingError(subfolderList, SUBFOLDER_A_PATH, PROJECT_PATH, subARows.getRows(), expectedError); + + // Cannot move rows from a list defined in a subfolder to a folder further down the hierarchy + expectedError = String.format("List '%s' is not accessible from folder %s.", subfolderList.getName(), SUBFOLDER_MINOR_A_PATH); + moveRowsExpectingError(subfolderList, SUBFOLDER_A_PATH, SUBFOLDER_MINOR_A_PATH, subARows.getRows(), expectedError); + } + + private void successfullyMoveRows(ListDefinition list, List> rows) throws Exception + { + var rowCount = rows.size(); + var rowAttachmentSize = attachmentCount(rows); + + var mid = rowCount / 2; + var firstRows = rows.subList(0, mid); + var firstRowsSize = firstRows.size(); + var firstRowAttachmentSize = attachmentCount(firstRows); + + var secondRows = rows.subList(mid, rowCount); + var secondRowsSize = secondRows.size(); + var secondRowAttachmentSize = attachmentCount(secondRows); + + // Act + var moveResponse = moveRows(list, getProjectName(), SUBFOLDER_A_PATH, rows); + var expectedCounts = new UpdateCounts(rowAttachmentSize, rowCount + 2, rowCount, rowCount, rowCount - rowAttachmentSize); + verifySuccessfulMove(list, moveResponse, getProjectName(), SUBFOLDER_A_PATH, expectedCounts); + if (checker().errorsSinceMark() > 0) + return; + + // Do it again with the same rows expecting no changes + moveResponse = moveRows(list, getProjectName(), SUBFOLDER_A_PATH, rows); + verifySuccessfulMove(list, moveResponse, getProjectName(), SUBFOLDER_A_PATH, NO_UPDATE); + if (checker().errorsSinceMark() > 0) + return; + + // Attempt to move some rows from the project to a different subfolder even though they have already been moved + moveResponse = moveRows(list, getProjectName(), SUBFOLDER_B_PATH, firstRows); + verifySuccessfulMove(list, moveResponse, getProjectName(), SUBFOLDER_B_PATH, NO_UPDATE); + if (checker().errorsSinceMark() > 0) + return; + + // Now move them between subfolders + moveResponse = moveRows(list, SUBFOLDER_A_PATH, SUBFOLDER_B_PATH, firstRows); + expectedCounts = new UpdateCounts(firstRowAttachmentSize, firstRowsSize + 2, firstRowsSize + firstRowsSize, firstRowsSize, firstRowsSize - firstRowAttachmentSize); + verifySuccessfulMove(list, moveResponse, SUBFOLDER_A_PATH, SUBFOLDER_B_PATH, expectedCounts); + if (checker().errorsSinceMark() > 0) + return; + + // Now move them between subfolders + moveResponse = moveRows(list, SUBFOLDER_A_PATH, SUBFOLDER_B_PATH, secondRows); + expectedCounts = new UpdateCounts(secondRowAttachmentSize, secondRowsSize + 2, secondRowsSize + secondRowsSize, secondRowsSize, secondRowsSize - secondRowAttachmentSize); + verifySuccessfulMove(list, moveResponse, SUBFOLDER_A_PATH, SUBFOLDER_B_PATH, expectedCounts); + if (checker().errorsSinceMark() > 0) + return; + + // Now move them further down the hierarchy + moveResponse = moveRows(list, SUBFOLDER_B_PATH, SUBFOLDER_MINOR_A_PATH, secondRows); + expectedCounts = new UpdateCounts(secondRowAttachmentSize, secondRowsSize + 2, secondRowsSize * 3, secondRowsSize, secondRowsSize - secondRowAttachmentSize); + verifySuccessfulMove(list, moveResponse, SUBFOLDER_B_PATH, SUBFOLDER_MINOR_A_PATH, expectedCounts); + } + + private int attachmentCount(List> rows) + { + return (int) rows.stream().filter(row -> row.get(attachmentFieldName) != null).count(); + } + + private void verifySuccessfulMove( + ListDefinition list, + MoveRowsResponse response, + String sourceContainerPath, + String targetContainerPath, + UpdateCounts expectedCounts + ) throws IOException, CommandException + { + final String sourcePath = sourceContainerPath.startsWith("/") ? sourceContainerPath : "/" + sourceContainerPath; + final String targetPath = targetContainerPath.startsWith("/") ? targetContainerPath : "/" + targetContainerPath; + + // Verify response + checker().verifyTrue("Expected successful move rows response", response.getSuccess()); + checker().verifyTrue("Expected a transaction audit Id", response.getTransactionAuditId() > 0); + checker().verifyEquals("Unexpected container path", targetPath, response.getContainerPath()); + + // Verify update counts + checker().wrapAssertion(() -> Assertions.assertThat(response.getUpdateCounts().keySet()) + .as("Expect list moveRows response to contain specific update counts") + .containsExactlyInAnyOrder("fileAttachmentsMoved", "listAuditEventsCreated", "listAuditEventsMoved", "listRecords", "queryAuditEventsMoved")); + checker().verifyEquals("Unexpected number of file attachments moved", expectedCounts.fileAttachmentsMoved, response.getUpdateCounts().get("fileAttachmentsMoved")); + checker().verifyEquals("Unexpected number of list audit events created", expectedCounts.listAuditEventsCreated, response.getUpdateCounts().get("listAuditEventsCreated")); + checker().verifyEquals("Unexpected number of list audit events moved", expectedCounts.listAuditEventsMoved, response.getUpdateCounts().get("listAuditEventsMoved")); + checker().verifyEquals("Unexpected number of list records moved", expectedCounts.listRecords, response.getUpdateCounts().get("listRecords")); + checker().verifyEquals("Unexpected number of query audit events moved", expectedCounts.queryAuditEventsMoved, response.getUpdateCounts().get("queryAuditEventsMoved")); + + // Verify audit logs + var hasUpdates = expectedCounts != NO_UPDATE; + var listAuditEvents = _auditLogHelper.getAuditLogsForTransactionId(getProjectName(), LIST_AUDIT_EVENT, List.of("Comment", "Container/Path", "ListId"), response.getTransactionAuditId(), ContainerFilter.CurrentAndSubfolders); + checker().verifyEquals("Unexpected number of list audit events", expectedCounts.listAuditEventsCreated, listAuditEvents.size()); + checker().wrapAssertion(() -> { + var matches = auditEventMatches(listAuditEvents, targetPath, list.getListId(), String.format("Moved %d rows from %s", expectedCounts.listRecords, sourcePath)); + Assertions.assertThat(matches) + .as("Expected one event recording move in target container") + .hasSize(hasUpdates ? 1 : 0); + }); + checker().wrapAssertion(() -> { + var matches = auditEventMatches(listAuditEvents, sourcePath, list.getListId(), String.format("Moved %d rows to %s", expectedCounts.listRecords, targetPath)); + Assertions.assertThat(matches) + .as("Expected one event recording move in source container") + .hasSize(hasUpdates ? 1 : 0); + }); + checker().wrapAssertion(() -> { + var matches = auditEventMatches(listAuditEvents, targetPath, list.getListId(), "An existing list record was moved"); + Assertions.assertThat(matches) + .as("Expected each list record move to be audited in the target container") + .hasSize(expectedCounts.listRecords); + }); + } + + private List> auditEventMatches(List> events, String containerPath, Integer listId, String comment) + { + return events.stream() + .filter((event) -> + comment.equals(event.get("Comment")) && + containerPath.equals(event.get("Container/Path")) && + listId.equals(event.get("ListId"))) + .toList(); + } + + private QueryApiHelper getQueryApiHelper(String containerPath, String listName) + { + return new QueryApiHelper(createDefaultConnection(), containerPath, LIST_SCHEMA, listName); + } + + private MoveRowsResponse moveRows(ListDefinition list, String sourceContainerPath, String targetContainerPath, List> rows) throws Exception + { + return getQueryApiHelper(sourceContainerPath, list.getName()) + .moveRows(rows, targetContainerPath); + } + + private void moveRowsExpectingError( + ListDefinition list, + String sourceContainerPath, + String targetContainerPath, + List> rows, + String expectedError + ) + { + String error = null; + + try + { + moveRows(list, sourceContainerPath, targetContainerPath, rows); + } + catch (Exception e) + { + error = e.getMessage(); + } + + checker().verifyEquals("Unexpected error message", expectedError, error); + } + + private SelectRowsResponse addRows(ListDefinition list, String containerPath, int numRows, int numRowsWithAttachmentValue) throws IOException, CommandException + { + // At this time, the only way to provide values for attachment fields on lists is via the UI. The following + // adds an initial set of rows without attachment values, then adds a second set of rows with attachment values. + // Each row with attachment values is entered manually through the UI form. + var dataGenerator = list.getTestDataGenerator(containerPath) + .addDataSupplier(stringKeyFieldName, () -> TestDataGenerator.randomString(TestDataGenerator.randomInt(21, 57))) + .addDataSupplier(attachmentFieldName, () -> null); + + dataGenerator.withGeneratedRows(numRows - numRowsWithAttachmentValue) + .insertRows(); + + var attachmentRows = dataGenerator.withGeneratedRows(numRowsWithAttachmentValue) + .getRows(); + + _listHelper.beginAtList(containerPath, list.getName()); + for (var row : attachmentRows) + { + var newRow = new CaseInsensitiveHashMap<>(); + newRow.putAll(row); + newRow.put(attachmentFieldName, SAMPLE_DATA_FILE); + _listHelper.insertNewRow(newRow, false); + } + + return getQueryApiHelper(containerPath, list.getName()) + .selectRows(); + } + + private List getFields() + { + return List.of( + new FieldDefinition(attachmentFieldName, FieldDefinition.ColumnType.Attachment), + new FieldDefinition(booleanFieldName, FieldDefinition.ColumnType.Boolean), + new FieldDefinition(dateFieldName, FieldDefinition.ColumnType.Date), + new FieldDefinition(dateTimeFieldName, FieldDefinition.ColumnType.DateAndTime), + new FieldDefinition(decimalFieldName, FieldDefinition.ColumnType.DateAndTime), + new FieldDefinition(integerFieldName, FieldDefinition.ColumnType.Integer) + ); + } + + private Integer resolveListId(Connection conn, String listName, String containerPath) throws IOException, CommandException + { + var cmd = new SelectRowsCommand("ListManager", "ListManager"); + cmd.setFilters(List.of(new Filter("Name", listName))); + cmd.setColumns(List.of("ListId")); + var response = cmd.execute(conn, containerPath); + + return (Integer) response.getRows().get(0).get("ListId"); + } + + private void truncateList(ListDefinition list) throws IOException, CommandException + { + var listName = list.getName(); + var connection = createDefaultConnection(); + var cmd = new TruncateTableCommand(LIST_SCHEMA, listName); + + cmd.execute(connection, PROJECT_PATH); + cmd.execute(connection, SUBFOLDER_A_PATH); + cmd.execute(connection, SUBFOLDER_MINOR_A_PATH); + cmd.execute(connection, SUBFOLDER_B_PATH); + } + + @Override + protected String getProjectName() + { + return PROJECT_NAME; + } + + @Override + public List getAssociatedModules() + { + return List.of("list"); + } +} diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index afab430cfd..f9f561c20c 100644 --- a/src/org/labkey/test/tests/list/ListTest.java +++ b/src/org/labkey/test/tests/list/ListTest.java @@ -33,7 +33,6 @@ import org.labkey.test.Locator; import org.labkey.test.SortDirection; import org.labkey.test.TestFileUtils; -import org.labkey.test.TestTimeoutException; import org.labkey.test.WebTestHelper; import org.labkey.test.categories.Daily; import org.labkey.test.categories.Data; @@ -226,13 +225,6 @@ private void doSetup() goToProjectHome(); } - @Override - protected void doCleanup(boolean afterTest) throws TestTimeoutException - { - _containerHelper.deleteProject(getProjectName(), afterTest); - _containerHelper.deleteProject(PROJECT_OTHER, afterTest); - } - @Before public void preTest() { diff --git a/src/org/labkey/test/util/query/MoveRowsResponse.java b/src/org/labkey/test/util/query/MoveRowsResponse.java new file mode 100644 index 0000000000..81b23f9b8a --- /dev/null +++ b/src/org/labkey/test/util/query/MoveRowsResponse.java @@ -0,0 +1,80 @@ +package org.labkey.test.util.query; + +import org.labkey.remoteapi.query.RowsResponse; + +import java.util.Collections; +import java.util.Map; + +public class MoveRowsResponse +{ + private final String _containerPath; + private final Boolean _reselectRowCount; + private final RowsResponse _rowsResponse; + private final Boolean _success; + private final Integer _transactionAuditId; + private final Map _updateCounts; + + public MoveRowsResponse(RowsResponse response) + { + _rowsResponse = response; + Map data = _rowsResponse.getParsedData(); + if (data == null) + data = Collections.emptyMap(); + + _containerPath = (String) data.get("containerPath"); + _reselectRowCount = (Boolean) data.get("reselectRowCount"); + _success = (Boolean) data.get("success"); + _transactionAuditId = (Integer) data.get("transactionAuditId"); + _updateCounts = (Map) data.get("updateCounts"); + } + + public String getCommand() + { + return _rowsResponse.getCommand(); + } + + public String getContainerPath() + { + return _containerPath; + } + + public String getQueryName() + { + return _rowsResponse.getQueryName(); + } + + public Boolean getReselectRowCount() + { + return _reselectRowCount; + } + + public Number getRowsAffected() + { + return _rowsResponse.getRowsAffected(); + } + + public RowsResponse getRowsResponse() + { + return _rowsResponse; + } + + public String getSchemaName() + { + return _rowsResponse.getSchemaName(); + } + + public Boolean getSuccess() + { + return _success; + } + + public Integer getTransactionAuditId() + { + return _transactionAuditId; + } + + public Map getUpdateCounts() + { + return _updateCounts; + } +} diff --git a/src/org/labkey/test/util/query/QueryApiHelper.java b/src/org/labkey/test/util/query/QueryApiHelper.java index 1032ef98a4..f810babc7d 100644 --- a/src/org/labkey/test/util/query/QueryApiHelper.java +++ b/src/org/labkey/test/util/query/QueryApiHelper.java @@ -39,7 +39,7 @@ public class QueryApiHelper private final String _schema; private final String _query; - private int _insertTimout = 180_000; + private int _insertTimeout = 180_000; public QueryApiHelper(Connection connection, String containerPath, String schema, String query) { @@ -49,9 +49,9 @@ public QueryApiHelper(Connection connection, String containerPath, String schema _query = query; } - public QueryApiHelper setInsertTimout(int insertTimout) + public QueryApiHelper setInsertTimeout(int insertTimeout) { - _insertTimout = insertTimout; + _insertTimeout = insertTimeout; return this; } @@ -93,7 +93,7 @@ public RowsResponse insertRows(List> rows) throws IOException { InsertRowsCommand insertRowsCommand = new InsertRowsCommand(_schema, _query); insertRowsCommand.setRows(makeApiRows(rows)); - insertRowsCommand.setTimeout(_insertTimout); + insertRowsCommand.setTimeout(_insertTimeout); insertRowsCommand.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); return insertRowsCommand.execute(_connection, _containerPath); } @@ -102,33 +102,34 @@ public RowsResponse updateRows(List> rows) throws IOException { UpdateRowsCommand updateRowsCommand = new UpdateRowsCommand(_schema, _query); updateRowsCommand.setRows(makeApiRows(rows)); - updateRowsCommand.setTimeout(_insertTimout); + updateRowsCommand.setTimeout(_insertTimeout); updateRowsCommand.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); - return updateRowsCommand.execute(_connection, _containerPath); + return updateRowsCommand.execute(_connection, _containerPath); } - public RowsResponse moveRows(List> rows, String targetContainerPath) throws IOException, CommandException + public MoveRowsResponse moveRows(List> rows, String targetContainerPath) throws IOException, CommandException { MoveRowsCommand moveRowsCommand = new MoveRowsCommand(targetContainerPath, _schema, _query); moveRowsCommand.setRows(makeApiRows(rows)); - moveRowsCommand.setTimeout(_insertTimout); + moveRowsCommand.setTimeout(_insertTimeout); moveRowsCommand.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); - return moveRowsCommand.execute(_connection, _containerPath); + RowsResponse response = moveRowsCommand.execute(_connection, _containerPath); + return new MoveRowsResponse(response); } public ImportDataResponse importData(String text) throws IOException, CommandException { ImportDataCommand importDataCommand = new ImportDataCommand(_schema, _query); importDataCommand.setText(text); - importDataCommand.setTimeout(_insertTimout); - return importDataCommand.execute(_connection, _containerPath); + importDataCommand.setTimeout(_insertTimeout); + return importDataCommand.execute(_connection, _containerPath); } public ImportDataResponse importData(File file) throws IOException, CommandException { ImportDataCommand importDataCommand = new ImportDataCommand(_schema, _query); importDataCommand.setFile(file); - importDataCommand.setTimeout(_insertTimout); + importDataCommand.setTimeout(_insertTimeout); return importDataCommand.execute(_connection, _containerPath); } @@ -141,7 +142,7 @@ public ImportDataResponse importExperimentData(String text, AuditLogHelper.Audit importDataCommand.setCrossTypeImport(isCrossType); importDataCommand.setText(text); importDataCommand.setInsertOption(insertOption); - importDataCommand.setTimeout(_insertTimout); + importDataCommand.setTimeout(_insertTimeout); return importDataCommand.execute(_connection, _containerPath); } @@ -189,5 +190,4 @@ public CommandResponse deleteDomain() throws IOException, CommandException DropDomainCommand delCmd = new DropDomainCommand(_schema, _query); return delCmd.execute(_connection, _containerPath); } - } From 06c9c0ad827e49b7a2eb45bc4920206086cf2bf2 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 2 Oct 2025 23:13:14 -0700 Subject: [PATCH 2/4] Additional tests --- .../labkey/test/tests/TriggerScriptTest.java | 114 +++++++++++++----- .../test/tests/list/ListMoveRowsTest.java | 17 ++- 2 files changed, 91 insertions(+), 40 deletions(-) diff --git a/src/org/labkey/test/tests/TriggerScriptTest.java b/src/org/labkey/test/tests/TriggerScriptTest.java index 2b4e2305ea..41dd5ddd77 100644 --- a/src/org/labkey/test/tests/TriggerScriptTest.java +++ b/src/org/labkey/test/tests/TriggerScriptTest.java @@ -15,6 +15,7 @@ */ package org.labkey.test.tests; +import org.assertj.core.api.Assertions; import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.junit.Before; @@ -26,6 +27,7 @@ import org.labkey.remoteapi.query.DeleteRowsCommand; import org.labkey.remoteapi.query.InsertRowsCommand; import org.labkey.remoteapi.query.BaseRowsCommand; +import org.labkey.remoteapi.query.MoveRowsCommand; import org.labkey.remoteapi.query.RowsResponse; import org.labkey.remoteapi.query.UpdateRowsCommand; import org.labkey.test.BaseWebDriverTest; @@ -40,6 +42,8 @@ import org.labkey.test.params.FieldDefinition.ColumnType; import org.labkey.test.params.experiment.DataClassDefinition; 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.Maps; import org.labkey.test.util.PortalHelper; @@ -61,6 +65,10 @@ @BaseWebDriverTest.ClassTimeout(minutes = 8) public class TriggerScriptTest extends BaseWebDriverTest { + private static final String PROJECT_NAME = "Test Trigger Script Project"; + private static final String SUBFOLDER_NAME = "SubfolderA"; + private static final String SUBFOLDER_PATH = "/" + PROJECT_NAME + "/" + SUBFOLDER_NAME; + //List constants private static final String TRIGGER_MODULE = "triggerTestModule"; private static final String SIMPLE_MODULE = "simpletest"; @@ -90,6 +98,7 @@ public class TriggerScriptTest extends BaseWebDriverTest public static final String PEOPLE_LIST_NAME = "People"; protected final PortalHelper _portalHelper = new PortalHelper(this); + private static ListDefinition EMPLOYEE_LIST; @Override public List getAssociatedModules() @@ -101,7 +110,7 @@ public List getAssociatedModules() @Override protected String getProjectName() { - return "Test Trigger Script Project"; + return PROJECT_NAME; } @Override @@ -167,36 +176,42 @@ private interface GoToDataUI } @BeforeClass - public static void projectSetup() + public static void projectSetup() throws Exception { TriggerScriptTest init = getCurrentTest(); init.doSetup(); } - - protected void doSetup() + protected void doSetup() throws Exception { _containerHelper.createProject(getProjectName(), null); + _containerHelper.createSubfolder(getProjectName(), SUBFOLDER_NAME); _containerHelper.enableModule(getProjectName(), "Query"); _containerHelper.enableModule(getProjectName(), SIMPLE_MODULE); _containerHelper.enableModule(getProjectName(), TRIGGER_MODULE); - //create List - FieldDefinition[] columns = new FieldDefinition[] { + // Create lists + { + List fields = List.of( new FieldDefinition("name", ColumnType.String).setLabel("Name"), new FieldDefinition("ssn", ColumnType.String).setLabel("SSN"), new FieldDefinition("company", ColumnType.String).setLabel("Company") + ); - }; - - _listHelper.createList(getProjectName(), LIST_NAME, "Key", columns); + EMPLOYEE_LIST = new IntListDefinition(LIST_NAME, "Key").setFields(fields); + EMPLOYEE_LIST.create(createDefaultConnection(), getProjectName()); - log("Create the People list"); - _listHelper.createList(getProjectName(), PEOPLE_LIST_NAME, "Key", + fields = List.of( new FieldDefinition("Name", ColumnType.String).setDescription("Name"), new FieldDefinition("Age", ColumnType.Integer).setDescription("Age"), new FieldDefinition("FavoriteDateTime", ColumnType.DateAndTime).setDescription("Favorite date time. Who doesn't have one?"), - new FieldDefinition("Crazy", ColumnType.Boolean).setDescription("Crazy?")); + new FieldDefinition("Crazy", ColumnType.Boolean).setDescription("Crazy?") + ); + + new IntListDefinition(PEOPLE_LIST_NAME, "Key") + .setFields(fields) + .create(createDefaultConnection(), getProjectName()); + } importFolderFromZip(TestFileUtils.getSampleData("studies/LabkeyDemoStudy.zip")); @@ -217,6 +232,7 @@ public void goToProjectStart() @Test public void testListIndividualTriggers() { + cleanUpListRows(); EmployeeRecord caughtAfter = new EmployeeRecord("Emp 1", "1112223333", "Test"), changedBefore = new EmployeeRecord("Emp 2", "2223334444", "Some Other"); @@ -267,12 +283,12 @@ public void testListIndividualTriggers() clickButton("Back"); //Verify validation error prevented delete waitForElement(Locator.tagWithText("td", "Emp 3")); - cleanUpListRows(); } @Test public void testListImportTriggers() { + cleanUpListRows(); goToManagedList(LIST_NAME); _listHelper.clickImportData(); @@ -302,7 +318,27 @@ public void testListImportTriggers() importDataPage.submit(); waitForElement(Locator.tagWithText("td","Importing TSV")); + } + + @Test + public void testListMoveTriggers() throws Exception + { cleanUpListRows(); + + RowsResponse response = EMPLOYEE_LIST.getTestDataGenerator(getProjectName()) + .addCustomRow(Map.of("name", "Emp 11", "ssn", "123-45-6789", "company", "LK")) + .insertRows(); + + List records = response.getRows().stream().map(EmployeeRecord::fromMap).toList(); + + openServerJavaScriptConsole(); + + MoveRowsCommand command = new MoveRowsCommand(SUBFOLDER_PATH, LIST_SCHEMA, LIST_NAME); + command.setRows(List.of(Map.of("Key", records.get(0).key))); + command.execute(createDefaultConnection(), getProjectName()); + waitForConsole("init got triggered with event: move", "complete got triggered with event: move"); + + closeServerJavaScriptConsole(); } /** Issue 52098 - ensure trigger scripts have a chance to do custom type conversion with the incoming row */ @@ -336,9 +372,10 @@ public void testListAPITriggerTypeConversion() throws Exception Assert.assertEquals(26, updatedRow.get("Age")); } - @Test + @Test public void testListAPITriggers() throws Exception { + cleanUpListRows(); String ssn1 = "111111112"; String ssn2 = "222211111"; @@ -408,8 +445,6 @@ public void testListAPITriggers() throws Exception delCmd = new DeleteRowsCommand(LIST_SCHEMA, LIST_NAME); delCmd.addRow(row3.toMap()); assertAPIErrorMessage(delCmd, BEFORE_DELETE_ERROR, cn); - - cleanUpListRows(); } /******************************** @@ -470,7 +505,7 @@ public void testDatasetImportTriggers() @Test public void testDatasetAPITriggers() throws Exception { - doAPITriggerTest(STUDY_SCHEMA,DATASET_NAME,"ParticipantId", true); + doAPITriggerTest(STUDY_SCHEMA, DATASET_NAME, "ParticipantId", true); } /******************************** @@ -484,22 +519,14 @@ public void testDataClassIndividualTriggers() throws Exception GoToDataUI goToDataClass = () -> goTo("Data Classes", DATA_CLASSES_NAME); setupDataClass(); + openServerJavaScriptConsole(); - // Go to the log view to start capturing messages - new SiteNavBar(getDriver()).clickAdminMenuItem(false, "Developer Links", "Server JavaScript Console"); - switchToWindow(1); - waitForText("Message"); - - switchToMainWindow(); doIndividualTriggerTest("query", goToDataClass, "Name", false, "Yes, Delete", false); - // Go back to the console window - switchToWindow(1); - waitForText("init got triggered with event: delete", + waitForConsole("init got triggered with event: delete", "exp.data: this is from the shared function", "complete got triggered with event: delete"); - getDriver().close(); - switchToMainWindow(); + closeServerJavaScriptConsole(); } @@ -728,7 +755,9 @@ private void assertAPIErrorMessage(BaseRowsCommand cmd, String expected, Connect } catch (CommandException e) { - Assert.assertTrue("Trigger script error message was wrong", e.getMessage().contains(expected)); + Assertions.assertThat(e.getMessage()) + .as("Trigger script error should contain expected text") + .contains(expected); } } @@ -834,7 +863,7 @@ private void goTo(String webPartName, String tableName) /** * Generate delimited string of keys from a map */ - private String joinMapValues(Map data, String delimiter ) + private String joinMapValues(Map data, String delimiter) { StringBuilder sb = new StringBuilder(); data.values().forEach(val -> sb.append(val).append(delimiter)); @@ -844,7 +873,7 @@ private String joinMapValues(Map data, String delimiter ) /** * Generate delimited string of keys from a map */ - private String joinMapKeys(Map data, String delimiter ) + private String joinMapKeys(Map data, String delimiter) { StringBuilder sb = new StringBuilder(); data.keySet().forEach(val -> sb.append(val).append(delimiter)); @@ -885,4 +914,27 @@ private void setupSampleType() new FieldDefinition(COUNTRY_FIELD, ColumnType.String))); SampleTypeAPIHelper.createEmptySampleType(getProjectName(), sampleType); } + + private void closeServerJavaScriptConsole() + { + switchToWindow(1); + getDriver().close(); + switchToMainWindow(); + } + + private void openServerJavaScriptConsole() + { + // Go to the log view to start capturing messages + new SiteNavBar(getDriver()).clickAdminMenuItem(false, "Developer Links", "Server JavaScript Console"); + switchToWindow(1); + waitForText("Message"); + switchToMainWindow(); + } + + private void waitForConsole(String... text) + { + switchToWindow(1); + waitForText(text); + switchToMainWindow(); + } } diff --git a/src/org/labkey/test/tests/list/ListMoveRowsTest.java b/src/org/labkey/test/tests/list/ListMoveRowsTest.java index c3e88127f5..2820eb4846 100644 --- a/src/org/labkey/test/tests/list/ListMoveRowsTest.java +++ b/src/org/labkey/test/tests/list/ListMoveRowsTest.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.IOException; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -159,7 +160,6 @@ protected void doCleanup(boolean afterTest) @Test public void testAutoIntListMove() throws Exception { - // Arrange truncateList(AUTO_INCREMENT_LIST); int totalRows = 100; @@ -172,7 +172,6 @@ public void testAutoIntListMove() throws Exception @Test public void testIntListMove() throws Exception { - // Arrange truncateList(INTEGER_KEY_LIST); int totalRows = 100; @@ -185,7 +184,6 @@ public void testIntListMove() throws Exception @Test public void testVarListMove() throws Exception { - // Arrange truncateList(STRING_KEY_LIST); int totalRows = 100; @@ -283,21 +281,21 @@ public void testListInSubfolder() throws Exception private void successfullyMoveRows(ListDefinition list, List> rows) throws Exception { - var rowCount = rows.size(); + var rowsSize = rows.size(); var rowAttachmentSize = attachmentCount(rows); - var mid = rowCount / 2; + var mid = rowsSize / 2; var firstRows = rows.subList(0, mid); var firstRowsSize = firstRows.size(); var firstRowAttachmentSize = attachmentCount(firstRows); - var secondRows = rows.subList(mid, rowCount); + var secondRows = rows.subList(mid, rowsSize); var secondRowsSize = secondRows.size(); var secondRowAttachmentSize = attachmentCount(secondRows); // Act var moveResponse = moveRows(list, getProjectName(), SUBFOLDER_A_PATH, rows); - var expectedCounts = new UpdateCounts(rowAttachmentSize, rowCount + 2, rowCount, rowCount, rowCount - rowAttachmentSize); + var expectedCounts = new UpdateCounts(rowAttachmentSize, rowsSize + 2, rowsSize, rowsSize, rowsSize - rowAttachmentSize); verifySuccessfulMove(list, moveResponse, getProjectName(), SUBFOLDER_A_PATH, expectedCounts); if (checker().errorsSinceMark() > 0) return; @@ -366,17 +364,18 @@ private void verifySuccessfulMove( checker().verifyEquals("Unexpected number of query audit events moved", expectedCounts.queryAuditEventsMoved, response.getUpdateCounts().get("queryAuditEventsMoved")); // Verify audit logs + var decimalFormat = new DecimalFormat("#,##0"); var hasUpdates = expectedCounts != NO_UPDATE; var listAuditEvents = _auditLogHelper.getAuditLogsForTransactionId(getProjectName(), LIST_AUDIT_EVENT, List.of("Comment", "Container/Path", "ListId"), response.getTransactionAuditId(), ContainerFilter.CurrentAndSubfolders); checker().verifyEquals("Unexpected number of list audit events", expectedCounts.listAuditEventsCreated, listAuditEvents.size()); checker().wrapAssertion(() -> { - var matches = auditEventMatches(listAuditEvents, targetPath, list.getListId(), String.format("Moved %d rows from %s", expectedCounts.listRecords, sourcePath)); + var matches = auditEventMatches(listAuditEvents, targetPath, list.getListId(), String.format("Moved %s rows from %s", decimalFormat.format(expectedCounts.listRecords), sourcePath)); Assertions.assertThat(matches) .as("Expected one event recording move in target container") .hasSize(hasUpdates ? 1 : 0); }); checker().wrapAssertion(() -> { - var matches = auditEventMatches(listAuditEvents, sourcePath, list.getListId(), String.format("Moved %d rows to %s", expectedCounts.listRecords, targetPath)); + var matches = auditEventMatches(listAuditEvents, sourcePath, list.getListId(), String.format("Moved %s rows to %s", decimalFormat.format(expectedCounts.listRecords), targetPath)); Assertions.assertThat(matches) .as("Expected one event recording move in source container") .hasSize(hasUpdates ? 1 : 0); From fbf17732cff67368c6211cc2b8eee4572da23703 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 3 Oct 2025 12:19:43 -0700 Subject: [PATCH 3/4] More tests --- .../test/tests/list/ListMoveRowsTest.java | 127 +++++++++++++----- .../test/util/query/QueryApiHelper.java | 12 +- 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/src/org/labkey/test/tests/list/ListMoveRowsTest.java b/src/org/labkey/test/tests/list/ListMoveRowsTest.java index 2820eb4846..21e06b9400 100644 --- a/src/org/labkey/test/tests/list/ListMoveRowsTest.java +++ b/src/org/labkey/test/tests/list/ListMoveRowsTest.java @@ -7,8 +7,10 @@ import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.query.BaseRowsCommand; import org.labkey.remoteapi.query.ContainerFilter; import org.labkey.remoteapi.query.Filter; +import org.labkey.remoteapi.query.MoveRowsCommand; import org.labkey.remoteapi.query.SelectRowsCommand; import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.remoteapi.query.TruncateTableCommand; @@ -279,6 +281,42 @@ public void testListInSubfolder() throws Exception moveRowsExpectingError(subfolderList, SUBFOLDER_A_PATH, SUBFOLDER_MINOR_A_PATH, subARows.getRows(), expectedError); } + @Test + public void testAuditDetailsNone() throws Exception + { + truncateList(STRING_KEY_LIST); + + // Arrange + int totalRows = 5; + int numRowsWithAttachmentValue = 2; + var response = addRows(STRING_KEY_LIST, SUBFOLDER_A_PATH, totalRows, numRowsWithAttachmentValue); + + QueryApiHelper queryApiHelper = getQueryApiHelper(SUBFOLDER_A_PATH, STRING_KEY_LIST); + MoveRowsCommand command = queryApiHelper.createMoveRowsCommand(response.getRows(), SUBFOLDER_B_PATH); + command.setAuditBehavior(BaseRowsCommand.AuditBehavior.NONE); + + // Act + MoveRowsResponse moveRowsResponse = queryApiHelper.moveRows(command); + + // Assert + UpdateCounts updateCount = new UpdateCounts(numRowsWithAttachmentValue, 0, totalRows, totalRows, totalRows - numRowsWithAttachmentValue); + checker().verifyNull("Expected a null transaction audit Id", moveRowsResponse.getTransactionAuditId()); + verifyUpdateCounts(updateCount, moveRowsResponse.getUpdateCounts()); + } + + @Test + public void testInvalidArguments() throws Exception + { + truncateList(AUTO_INCREMENT_LIST); + var response = addRows(AUTO_INCREMENT_LIST, getProjectName(), 1, 0); + var validId = response.getRows().get(0).get(autoIncrementKeyFieldName); + + moveRowsExpectingError(AUTO_INCREMENT_LIST, getProjectName(), SUBFOLDER_B_PATH, List.of(), "No 'rows' array supplied."); + moveRowsExpectingError(AUTO_INCREMENT_LIST, getProjectName(), SUBFOLDER_B_PATH, List.of(Map.of("InvalidKey", validId)), "Key field value required for moving list rows."); + moveRowsExpectingError(AUTO_INCREMENT_LIST, getProjectName(), null, response.getRows(), "A target container must be specified for the move operation."); + moveRowsExpectingError(AUTO_INCREMENT_LIST, getProjectName(), "/Shared", response.getRows(), "Invalid target container for the move operation: /Shared."); + } + private void successfullyMoveRows(ListDefinition list, List> rows) throws Exception { var rowsSize = rows.size(); @@ -306,12 +344,6 @@ private void successfullyMoveRows(ListDefinition list, List> if (checker().errorsSinceMark() > 0) return; - // Attempt to move some rows from the project to a different subfolder even though they have already been moved - moveResponse = moveRows(list, getProjectName(), SUBFOLDER_B_PATH, firstRows); - verifySuccessfulMove(list, moveResponse, getProjectName(), SUBFOLDER_B_PATH, NO_UPDATE); - if (checker().errorsSinceMark() > 0) - return; - // Now move them between subfolders moveResponse = moveRows(list, SUBFOLDER_A_PATH, SUBFOLDER_B_PATH, firstRows); expectedCounts = new UpdateCounts(firstRowAttachmentSize, firstRowsSize + 2, firstRowsSize + firstRowsSize, firstRowsSize, firstRowsSize - firstRowAttachmentSize); @@ -330,6 +362,13 @@ private void successfullyMoveRows(ListDefinition list, List> moveResponse = moveRows(list, SUBFOLDER_B_PATH, SUBFOLDER_MINOR_A_PATH, secondRows); expectedCounts = new UpdateCounts(secondRowAttachmentSize, secondRowsSize + 2, secondRowsSize * 3, secondRowsSize, secondRowsSize - secondRowAttachmentSize); verifySuccessfulMove(list, moveResponse, SUBFOLDER_B_PATH, SUBFOLDER_MINOR_A_PATH, expectedCounts); + if (checker().errorsSinceMark() > 0) + return; + + // Now move all rows back to the project + moveResponse = moveRows(list, PROJECT_PATH, PROJECT_PATH, rows); + expectedCounts = new UpdateCounts(rowAttachmentSize, rowsSize + 4, (rowsSize * 3) + secondRowsSize, rowsSize, rowsSize - rowAttachmentSize); + verifySuccessfulMove(list, moveResponse, Map.of(SUBFOLDER_B_PATH, secondRowsSize, SUBFOLDER_MINOR_A_PATH, secondRowsSize), PROJECT_PATH, expectedCounts); } private int attachmentCount(List> rows) @@ -345,7 +384,17 @@ private void verifySuccessfulMove( UpdateCounts expectedCounts ) throws IOException, CommandException { - final String sourcePath = sourceContainerPath.startsWith("/") ? sourceContainerPath : "/" + sourceContainerPath; + verifySuccessfulMove(list, response, Map.of(sourceContainerPath, expectedCounts.listRecords), targetContainerPath, expectedCounts); + } + + private void verifySuccessfulMove( + ListDefinition list, + MoveRowsResponse response, + Map sourceContainerPathCounts, + String targetContainerPath, + UpdateCounts expectedCounts + ) throws IOException, CommandException + { final String targetPath = targetContainerPath.startsWith("/") ? targetContainerPath : "/" + targetContainerPath; // Verify response @@ -354,32 +403,36 @@ private void verifySuccessfulMove( checker().verifyEquals("Unexpected container path", targetPath, response.getContainerPath()); // Verify update counts - checker().wrapAssertion(() -> Assertions.assertThat(response.getUpdateCounts().keySet()) - .as("Expect list moveRows response to contain specific update counts") - .containsExactlyInAnyOrder("fileAttachmentsMoved", "listAuditEventsCreated", "listAuditEventsMoved", "listRecords", "queryAuditEventsMoved")); - checker().verifyEquals("Unexpected number of file attachments moved", expectedCounts.fileAttachmentsMoved, response.getUpdateCounts().get("fileAttachmentsMoved")); - checker().verifyEquals("Unexpected number of list audit events created", expectedCounts.listAuditEventsCreated, response.getUpdateCounts().get("listAuditEventsCreated")); - checker().verifyEquals("Unexpected number of list audit events moved", expectedCounts.listAuditEventsMoved, response.getUpdateCounts().get("listAuditEventsMoved")); - checker().verifyEquals("Unexpected number of list records moved", expectedCounts.listRecords, response.getUpdateCounts().get("listRecords")); - checker().verifyEquals("Unexpected number of query audit events moved", expectedCounts.queryAuditEventsMoved, response.getUpdateCounts().get("queryAuditEventsMoved")); + verifyUpdateCounts(expectedCounts, response.getUpdateCounts()); // Verify audit logs var decimalFormat = new DecimalFormat("#,##0"); var hasUpdates = expectedCounts != NO_UPDATE; var listAuditEvents = _auditLogHelper.getAuditLogsForTransactionId(getProjectName(), LIST_AUDIT_EVENT, List.of("Comment", "Container/Path", "ListId"), response.getTransactionAuditId(), ContainerFilter.CurrentAndSubfolders); checker().verifyEquals("Unexpected number of list audit events", expectedCounts.listAuditEventsCreated, listAuditEvents.size()); - checker().wrapAssertion(() -> { - var matches = auditEventMatches(listAuditEvents, targetPath, list.getListId(), String.format("Moved %s rows from %s", decimalFormat.format(expectedCounts.listRecords), sourcePath)); - Assertions.assertThat(matches) - .as("Expected one event recording move in target container") - .hasSize(hasUpdates ? 1 : 0); - }); - checker().wrapAssertion(() -> { - var matches = auditEventMatches(listAuditEvents, sourcePath, list.getListId(), String.format("Moved %s rows to %s", decimalFormat.format(expectedCounts.listRecords), targetPath)); - Assertions.assertThat(matches) - .as("Expected one event recording move in source container") - .hasSize(hasUpdates ? 1 : 0); - }); + + for (var entry : sourceContainerPathCounts.entrySet()) + { + String sourcePath = entry.getKey().startsWith("/") ? entry.getKey() : "/" + entry.getKey(); + Integer expectedListRecordCount = entry.getValue(); + + checker().wrapAssertion(() -> { + var comment = String.format("Moved %s rows from %s", decimalFormat.format(expectedListRecordCount), sourcePath); + var matches = auditEventMatches(listAuditEvents, targetPath, list.getListId(), comment); + Assertions.assertThat(matches) + .as(String.format("Expected audit summary event in target container \"%s\"", comment)) + .hasSize(hasUpdates ? 1 : 0); + }); + + checker().wrapAssertion(() -> { + var comment = String.format("Moved %s rows to %s", decimalFormat.format(expectedListRecordCount), targetPath); + var matches = auditEventMatches(listAuditEvents, sourcePath, list.getListId(), comment); + Assertions.assertThat(matches) + .as(String.format("Expected audit summary event in source container \"%s\"", comment)) + .hasSize(hasUpdates ? 1 : 0); + }); + } + checker().wrapAssertion(() -> { var matches = auditEventMatches(listAuditEvents, targetPath, list.getListId(), "An existing list record was moved"); Assertions.assertThat(matches) @@ -388,6 +441,18 @@ private void verifySuccessfulMove( }); } + private void verifyUpdateCounts(UpdateCounts expectedCounts, Map responseCounts) + { + checker().wrapAssertion(() -> Assertions.assertThat(responseCounts.keySet()) + .as("Expect list moveRows response to contain specific update counts") + .containsExactlyInAnyOrder("fileAttachmentsMoved", "listAuditEventsCreated", "listAuditEventsMoved", "listRecords", "queryAuditEventsMoved")); + checker().verifyEquals("Unexpected number of file attachments moved", expectedCounts.fileAttachmentsMoved, responseCounts.get("fileAttachmentsMoved")); + checker().verifyEquals("Unexpected number of list audit events created", expectedCounts.listAuditEventsCreated, responseCounts.get("listAuditEventsCreated")); + checker().verifyEquals("Unexpected number of list audit events moved", expectedCounts.listAuditEventsMoved, responseCounts.get("listAuditEventsMoved")); + checker().verifyEquals("Unexpected number of list records moved", expectedCounts.listRecords, responseCounts.get("listRecords")); + checker().verifyEquals("Unexpected number of query audit events moved", expectedCounts.queryAuditEventsMoved, responseCounts.get("queryAuditEventsMoved")); + } + private List> auditEventMatches(List> events, String containerPath, Integer listId, String comment) { return events.stream() @@ -398,14 +463,14 @@ private List> auditEventMatches(List> ev .toList(); } - private QueryApiHelper getQueryApiHelper(String containerPath, String listName) + private QueryApiHelper getQueryApiHelper(String containerPath, ListDefinition list) { - return new QueryApiHelper(createDefaultConnection(), containerPath, LIST_SCHEMA, listName); + return new QueryApiHelper(createDefaultConnection(), containerPath, LIST_SCHEMA, list.getName()); } private MoveRowsResponse moveRows(ListDefinition list, String sourceContainerPath, String targetContainerPath, List> rows) throws Exception { - return getQueryApiHelper(sourceContainerPath, list.getName()) + return getQueryApiHelper(sourceContainerPath, list) .moveRows(rows, targetContainerPath); } @@ -455,7 +520,7 @@ private SelectRowsResponse addRows(ListDefinition list, String containerPath, in _listHelper.insertNewRow(newRow, false); } - return getQueryApiHelper(containerPath, list.getName()) + return getQueryApiHelper(containerPath, list) .selectRows(); } diff --git a/src/org/labkey/test/util/query/QueryApiHelper.java b/src/org/labkey/test/util/query/QueryApiHelper.java index f810babc7d..2ad532c595 100644 --- a/src/org/labkey/test/util/query/QueryApiHelper.java +++ b/src/org/labkey/test/util/query/QueryApiHelper.java @@ -107,12 +107,22 @@ public RowsResponse updateRows(List> rows) throws IOException return updateRowsCommand.execute(_connection, _containerPath); } - public MoveRowsResponse moveRows(List> rows, String targetContainerPath) throws IOException, CommandException + public MoveRowsCommand createMoveRowsCommand(List> rows, String targetContainerPath) { MoveRowsCommand moveRowsCommand = new MoveRowsCommand(targetContainerPath, _schema, _query); moveRowsCommand.setRows(makeApiRows(rows)); moveRowsCommand.setTimeout(_insertTimeout); moveRowsCommand.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); + return moveRowsCommand; + } + + public MoveRowsResponse moveRows(List> rows, String targetContainerPath) throws IOException, CommandException + { + return moveRows(createMoveRowsCommand(rows, targetContainerPath)); + } + + public MoveRowsResponse moveRows(MoveRowsCommand moveRowsCommand) throws IOException, CommandException + { RowsResponse response = moveRowsCommand.execute(_connection, _containerPath); return new MoveRowsResponse(response); } From bb7f5c819f3a0c163cb0512c9fd7c5451dc71fc4 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 3 Oct 2025 16:19:01 -0700 Subject: [PATCH 4/4] ListMoveRowsTest: add categories - match ListTest for now - there is talk of adding an API-based tests category --- src/org/labkey/test/tests/list/ListMoveRowsTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/tests/list/ListMoveRowsTest.java b/src/org/labkey/test/tests/list/ListMoveRowsTest.java index 21e06b9400..b96b6f9a66 100644 --- a/src/org/labkey/test/tests/list/ListMoveRowsTest.java +++ b/src/org/labkey/test/tests/list/ListMoveRowsTest.java @@ -16,6 +16,9 @@ import org.labkey.remoteapi.query.TruncateTableCommand; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.TestFileUtils; +import org.labkey.test.categories.Daily; +import org.labkey.test.categories.Data; +import org.labkey.test.categories.Hosting; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.list.IntListDefinition; import org.labkey.test.params.list.ListDefinition; @@ -39,7 +42,7 @@ import static org.labkey.test.util.PermissionsHelper.EDITOR_ROLE; import static org.labkey.test.util.PermissionsHelper.READER_ROLE; -@Category({}) +@Category({Daily.class, Data.class, Hosting.class}) // Matches ListTest for now public class ListMoveRowsTest extends BaseWebDriverTest { private static final String PROJECT_NAME = "ListMoveRowsTest";