diff --git a/modules/ETLtest/resources/ETLs/SourceToTarget2BulkLoad.xml b/modules/ETLtest/resources/ETLs/SourceToTarget2BulkLoad.xml new file mode 100644 index 0000000000..6b906aec7b --- /dev/null +++ b/modules/ETLtest/resources/ETLs/SourceToTarget2BulkLoad.xml @@ -0,0 +1,15 @@ + + + Source to target2 bulk load + append rows from source to target, skip detailed auditing + + + Copy to target2 + + + + + + + + diff --git a/modules/ETLtest/resources/ETLs/SourceToTarget2NoBulkLoad.xml b/modules/ETLtest/resources/ETLs/SourceToTarget2NoBulkLoad.xml new file mode 100644 index 0000000000..78a3118667 --- /dev/null +++ b/modules/ETLtest/resources/ETLs/SourceToTarget2NoBulkLoad.xml @@ -0,0 +1,15 @@ + + + Source to target2 no bulk load + append rows from source to target, with detailed auditing + + + Copy to target2 no bulkLoad + + + + + + + + \ No newline at end of file diff --git a/modules/ETLtest/resources/schemas/etltest.xml b/modules/ETLtest/resources/schemas/etltest.xml index a8326e4947..1abc4f056e 100644 --- a/modules/ETLtest/resources/schemas/etltest.xml +++ b/modules/ETLtest/resources/schemas/etltest.xml @@ -27,6 +27,7 @@ + DETAILED diff --git a/src/org/labkey/remoteapi/query/ImportExperimentDataCommand.java b/src/org/labkey/remoteapi/query/ImportExperimentDataCommand.java new file mode 100644 index 0000000000..9b5ac8b915 --- /dev/null +++ b/src/org/labkey/remoteapi/query/ImportExperimentDataCommand.java @@ -0,0 +1,78 @@ +package org.labkey.remoteapi.query; + +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.labkey.test.WebTestHelper; +import org.labkey.test.util.AuditLogHelper; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +public class ImportExperimentDataCommand extends ImportDataCommand +{ + private AuditLogHelper.AuditBehaviorType _auditBehavior; + private Boolean _crossTypeImport; + private Boolean _crossFolderImport; + private String _containerPath; + + public ImportExperimentDataCommand(String schemaName, String queryName, String containerPath) + { + super(schemaName, queryName); + _containerPath = containerPath; + } + + public AuditLogHelper.AuditBehaviorType getAuditBehavior() + { + return _auditBehavior; + } + + public void setAuditBehavior(AuditLogHelper.AuditBehaviorType auditBehavior) + { + _auditBehavior = auditBehavior; + } + + public Boolean getCrossTypeImport() + { + return _crossTypeImport; + } + + public void setCrossTypeImport(Boolean crossTypeImport) + { + _crossTypeImport = crossTypeImport; + } + + public Boolean getCrossFolderImport() + { + return _crossFolderImport; + } + + public void setCrossFolderImport(Boolean crossFolderImport) + { + _crossFolderImport = crossFolderImport; + } + + @Override + protected HttpPost createRequest(URI uri) { + HttpPost post = super.createRequest(uri); + String action = "samples".equalsIgnoreCase(getSchemaName()) ? "importSamples" : "importData"; + Map params = new HashMap<>(); + if (_auditBehavior != null) + params.put("auditBehavior", _auditBehavior.name()); + if (_crossTypeImport) + params.put("crossTypeImport", "true"); + if (_crossFolderImport) + params.put("crossFolderImport", "true"); + String url = WebTestHelper.buildURL("experiment", _containerPath, action, params); + try + { + post.setUri(new URI(url)); + } + catch (URISyntaxException e) + { + throw new RuntimeException(e); + } + return post; + } + +} diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index 6b34832353..ca319c26c4 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -75,6 +75,7 @@ import org.labkey.test.util.AbstractContainerHelper; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.ArtifactCollector; +import org.labkey.test.util.AuditLogHelper; import org.labkey.test.util.ComponentQuery; import org.labkey.test.util.Crawler; import org.labkey.test.util.CspLogUtil; @@ -2743,14 +2744,35 @@ public void dismissReleaseBanner(String productName, boolean dismiss) } protected void verifyQueryAPI(String schema, String dataType, Map row, boolean isInsert, String... errorMsg) + { + verifyQueryAPI(schema, dataType, null, row, isInsert, errorMsg); + } + + protected void verifyQueryAPI(String schema, String dataType, @Nullable AuditLogHelper.AuditBehaviorType auditBehavior, Map row, boolean isInsert, String... errorMsg) { String action = isInsert ? "insertRows" : "updateRows"; String updateScript = "LABKEY.Query." + action + "({ schemaName: \"" + schema + "\", "+ "queryName: " + EscapeUtil.toJSONStr(dataType) + ", " + + (auditBehavior == null ? "" : ("auditBehavior: " + EscapeUtil.toJSONStr(auditBehavior.name())) + ", ") + "success: callback," + "failure: callback," + "rows: [" + EscapeUtil.toJSONRow(row) + "]" + "})"; + log(updateScript); + executeAndVerifyScript(updateScript, errorMsg); + } + + protected void verifyQueryAPI(String schema, String dataType, @Nullable AuditLogHelper.AuditBehaviorType auditBehavior, List> rows, boolean isInsert, String... errorMsg) + { + String action = isInsert ? "insertRows" : "updateRows"; + String updateScript = "LABKEY.Query." + action + "({ schemaName: \"" + schema + "\", "+ + "queryName: " + EscapeUtil.toJSONStr(dataType) + ", " + + (auditBehavior == null ? "" : ("auditBehavior: " + EscapeUtil.toJSONStr(auditBehavior.name())) + ", ") + + "success: callback," + + "failure: callback," + + "rows: [" + EscapeUtil.toJSONRow(rows) + "]" + + "})"; + log(updateScript); executeAndVerifyScript(updateScript, errorMsg); } diff --git a/src/org/labkey/test/tests/AuditLogTest.java b/src/org/labkey/test/tests/AuditLogTest.java index aa429db57f..0a75098570 100644 --- a/src/org/labkey/test/tests/AuditLogTest.java +++ b/src/org/labkey/test/tests/AuditLogTest.java @@ -23,6 +23,7 @@ import org.junit.experimental.categories.Category; import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.query.BaseRowsCommand; import org.labkey.remoteapi.query.InsertRowsCommand; import org.labkey.remoteapi.query.RowsResponse; import org.labkey.test.BaseWebDriverTest; @@ -428,7 +429,7 @@ public void testDetailedQueryUpdateAuditLog() throws IOException, CommandExcepti { _containerHelper.createProject(AUDIT_DETAILED_TEST_PROJECT, "Custom"); _containerHelper.enableModule("simpletest"); - goToProjectHome(); + goToProjectHome(AUDIT_DETAILED_TEST_PROJECT); Connection cn = WebTestHelper.getRemoteApiConnection(); @@ -439,9 +440,43 @@ public void testDetailedQueryUpdateAuditLog() throws IOException, CommandExcepti insertCmd.addRow(rowMap); RowsResponse resp1 = insertCmd.execute(cn, AUDIT_DETAILED_TEST_PROJECT); + Integer transactionId = _auditLogHelper.checkAuditEventDiffCountForLastTransaction(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, 0, 1); + Map expectedValues = new HashMap<>(); + expectedValues.put("Comment", "1 row(s) were inserted."); + _auditLogHelper.checkAuditEventValuesForTransactionId(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, transactionId, 1, expectedValues); + goToProjectHome(AUDIT_DETAILED_TEST_PROJECT); + Map auditLog = getAuditLogRow(this, "Query update events", "Query Name", "Manufacturers"); assertEquals("Did not find expected audit log for summary log level", "1 row(s) were inserted.", auditLog.get("Comment")); + // create manufacturer (which has summary audit log level) with api audit override to detail + insertCmd = new InsertRowsCommand("vehicle", "manufacturers"); + insertCmd.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED); + rowMap = new HashMap<>(); + rowMap.put("name", "Kia_ev"); + insertCmd.addRow(rowMap); + insertCmd.execute(cn, AUDIT_DETAILED_TEST_PROJECT); + + goToProjectHome(AUDIT_DETAILED_TEST_PROJECT); + transactionId = _auditLogHelper.checkAuditEventDiffCountForLastTransaction(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, 7, 1); + expectedValues = new HashMap<>(); + expectedValues.put("Comment", "A row was inserted."); + _auditLogHelper.checkAuditEventValuesForTransactionId(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, transactionId, 1, expectedValues); + + // create manufacturer (which has summary audit log level) with api audit override to NONE. The override should be ignored + insertCmd = new InsertRowsCommand("vehicle", "manufacturers"); + insertCmd.setAuditBehavior(BaseRowsCommand.AuditBehavior.NONE); + rowMap = new HashMap<>(); + rowMap.put("name", "Kia_hybrid"); + insertCmd.addRow(rowMap); + insertCmd.execute(cn, AUDIT_DETAILED_TEST_PROJECT); + + goToProjectHome(AUDIT_DETAILED_TEST_PROJECT); + transactionId = _auditLogHelper.checkAuditEventDiffCountForLastTransaction(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, 0, 1); + expectedValues = new HashMap<>(); + expectedValues.put("Comment", "1 row(s) were inserted."); + _auditLogHelper.checkAuditEventValuesForTransactionId(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, transactionId, 1, expectedValues); + //then create model (which has detailed audit log level) InsertRowsCommand insertCmd2 = new InsertRowsCommand("vehicle", "models"); rowMap = new HashMap<>(); @@ -453,6 +488,20 @@ public void testDetailedQueryUpdateAuditLog() throws IOException, CommandExcepti refresh(); auditLog = getAuditLogRow(this, "Query update events", "Query Name", "Models"); assertEquals("Did not find expected audit log for detailed log level", "A row was inserted.", auditLog.get("Comment")); + goToProjectHome(AUDIT_DETAILED_TEST_PROJECT); + + // create model (which has detailed audit log level), with API audit SUMMARY, effective audit should be detailed + rowMap.put("name", "Carnival"); + insertCmd2 = new InsertRowsCommand("vehicle", "models"); + insertCmd2.setAuditBehavior(BaseRowsCommand.AuditBehavior.SUMMARY); + insertCmd2.addRow(rowMap); + insertCmd2.execute(cn, AUDIT_DETAILED_TEST_PROJECT); + + transactionId = _auditLogHelper.checkAuditEventDiffCountForLastTransaction(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, 8, 1); + expectedValues = new HashMap<>(); + expectedValues.put("Comment", "A row was inserted."); + _auditLogHelper.checkAuditEventValuesForTransactionId(AUDIT_DETAILED_TEST_PROJECT, AuditLogHelper.AuditEvent.QUERY_UPDATE_AUDIT_EVENT, transactionId, 1, expectedValues); + _containerHelper.deleteProject(AUDIT_DETAILED_TEST_PROJECT, false); } else diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index b25c892025..4962306eb9 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -53,6 +53,7 @@ import org.labkey.test.params.FieldInfo; import org.labkey.test.params.experiment.SampleTypeDefinition; import org.labkey.test.util.ApiPermissionsHelper; +import org.labkey.test.util.AuditLogHelper; import org.labkey.test.util.DataRegionExportHelper; import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.EscapeUtil; @@ -296,7 +297,7 @@ public void testCustomProperties() // Issue 47280: LKSM: Trailing/Leading whitespace in Source name won't resolve when deriving samples @Test - public void testImportSamplesWithTrailingSpace() + public void testImportSamplesWithTrailingSpace() throws IOException, CommandException { final String sampleTypeName = "SampleTypeWithProvidedName"; final List fields = List.of( @@ -317,6 +318,12 @@ public void testImportSamplesWithTrailingSpace() Map fieldMap = Map.of("Name", " S-1 ", "StringCol", "Ess ", "IntCol", "1 "); sampleTypeHelper.insertRow(fieldMap); + AuditLogHelper auditLogHelper = new AuditLogHelper(this); + int transactionId = auditLogHelper.checkAuditEventDiffCountForLastTransaction(getProjectName(), AuditLogHelper.AuditEvent.SAMPLE_TIMELINE_EVENT, 21, 1); + MapexpectedValues = new HashMap<>(); + expectedValues.put("Comment", "Sample was registered."); + auditLogHelper.checkAuditEventValuesForTransactionId(getProjectName(), AuditLogHelper.AuditEvent.SAMPLE_TIMELINE_EVENT, transactionId, 1, expectedValues); + log("Verify values were saved are without trailing spaces"); sampleTypeHelper.verifyDataValues(Collections.singletonList(fieldMap)); diff --git a/src/org/labkey/test/util/AuditLogHelper.java b/src/org/labkey/test/util/AuditLogHelper.java index 8a5b3d8fc5..798743d4af 100644 --- a/src/org/labkey/test/util/AuditLogHelper.java +++ b/src/org/labkey/test/util/AuditLogHelper.java @@ -53,6 +53,13 @@ public class AuditLogHelper "Comment" ); + public static final String SCHEMA_XML_AUDIT = "\n" + + "
\n" + + " %s\n" + + "
\n" + + "\n"; + + private final WebDriverWrapper _wrapper; private final ConnectionSupplier _connectionSupplier; @@ -67,6 +74,13 @@ public AuditLogHelper(WebDriverWrapper wrapper) this(wrapper, wrapper::createDefaultConnection); } + public enum AuditBehaviorType + { + NONE, + DETAILED, + SUMMARY; + } + public enum AuditEvent { SAMPLE_TIMELINE_EVENT("SampleTimelineEvent"), @@ -74,8 +88,10 @@ public enum AuditEvent INVENTORY_AUDIT_EVENT("InventoryAuditEvent"), LIST_AUDIT_EVENT("ListAuditEvent"), ASSAY_AUDIT_EVENT("AssayAuditEvent"), // avaialble with SampleManagement module + ASSAY_RESULT_AUDIT_EVENT("AssayResultAuditEvent"), // avaialble with SampleManagement module EXPERIMENT_AUDIT_EVENT("ExperimentAuditEvent"), SAMPLE_WORKFLOW_AUDIT_EVENT("SamplesWorkflowAuditEvent"), + QUERY_UPDATE_AUDIT_EVENT("QueryUpdateAuditEvent"), FILE_SYSTEM_EVENT("FileSystem"); private final String _name; @@ -158,16 +174,31 @@ public SelectRowsResponse getAuditLogsFromLKS(String containerPath, AuditEvent a } public List> getAuditLogsForTransactionId(String containerPath, AuditEvent auditEventName, List columnNames, - Integer transactionId, @Nullable ContainerFilter containerFilter) throws IOException, CommandException + Integer transactionId, @Nullable ContainerFilter containerFilter) throws IOException, CommandException + { + return getAuditLogsForTransactionId(containerPath, auditEventName, columnNames, transactionId, null, containerFilter); + } + + public List> getAuditLogsForTransactionId(String containerPath, AuditEvent auditEventName, List columnNames, + Integer transactionId, List eventFilters, @Nullable ContainerFilter containerFilter) throws IOException, CommandException { - List transactionFilter = List.of(new Filter("TransactionId", transactionId, Filter.Operator.EQUAL)); + List transactionFilter = new ArrayList<>(); + if (transactionId != null) + transactionFilter.add(new Filter("TransactionId", transactionId, Filter.Operator.EQUAL)); + if (eventFilters != null && !eventFilters.isEmpty()) + transactionFilter.addAll(eventFilters); return getAuditLogsFromLKS(containerPath, auditEventName, columnNames, transactionFilter, null, containerFilter).getRows(); } public void checkAuditEventValuesForTransactionId(String containerPath, AuditEvent auditEventName, Integer transactionId, int rowCount, Map expectedValues) throws IOException, CommandException + { + checkAuditEventValuesForTransactionId(containerPath, auditEventName, transactionId, null, rowCount, expectedValues); + } + + public void checkAuditEventValuesForTransactionId(String containerPath, AuditEvent auditEventName, Integer transactionId, List eventFilters, int rowCount, Map expectedValues) throws IOException, CommandException { List columnNames = expectedValues.keySet().stream().map(Object::toString).toList(); - List> events = getAuditLogsForTransactionId(containerPath, auditEventName, columnNames, transactionId, ContainerFilter.CurrentAndSubfolders); + List> events = getAuditLogsForTransactionId(containerPath, auditEventName, columnNames, transactionId, eventFilters, ContainerFilter.CurrentAndSubfolders); assertEquals("Unexpected number of events for transactionId " + transactionId, rowCount, events.size()); for (int i = 0; i < rowCount; i++) { @@ -250,6 +281,19 @@ public Integer getLastTransactionId(String containerPath, AuditEvent auditEventN } } + public Integer getLastEventId(String containerPath, AuditEvent auditEventName) + { + try + { + List> events = getAuditLogsFromLKS(containerPath, auditEventName, List.of("RowId"), Collections.emptyList(), 1, ContainerFilter.CurrentAndSubfolders).getRows(); + return events.size() == 1 ? (Integer) events.get(0).get("RowId") : null; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + public Integer doAndWaitForTransaction(Runnable action, String containerPath, AuditEvent auditEventName) { int prevTransactionId; @@ -272,16 +316,26 @@ public Integer doAndWaitForTransaction(Runnable action, String containerPath, Au }, "Error waiting for next transactionId in " + auditEventName, WAIT_FOR_JAVASCRIPT); } + public Integer checkAuditEventDiffCountForLastTransaction(String containerPath, AuditEvent auditEventName, int expectedDiffCount, + @Nullable Integer expectedEventCount) throws IOException, CommandException + { + return checkAuditEventDiffCountForLastTransaction(containerPath, auditEventName, Collections.emptyList(), expectedDiffCount, expectedEventCount); + } + /** * Check for the expected number of diffs in the audit event for the last transactionId. * If an expectedEventCount is also provided, it will check that the number of events for that transactionId matches the expectedEventCount. * @return transactionId */ - public Integer checkAuditEventDiffCountForLastTransaction(String containerPath, AuditEvent auditEventName, int expectedDiffCount, + public Integer checkAuditEventDiffCountForLastTransaction(String containerPath, AuditEvent auditEventName, @Nullable List eventFilters, int expectedDiffCount, @Nullable Integer expectedEventCount) throws IOException, CommandException { Integer transactionId = getLastTransactionId(containerPath, auditEventName); - List transactionFilter = List.of(new Filter("TransactionId", transactionId, Filter.Operator.EQUAL)); + List transactionFilter = new ArrayList<>(); + if (transactionId != null) + transactionFilter.add(new Filter("TransactionId", transactionId, Filter.Operator.EQUAL)); + if (eventFilters != null && !eventFilters.isEmpty()) + transactionFilter.addAll(eventFilters); List> events = getAuditLogsFromLKS(containerPath, auditEventName, List.of("Comment", "UserComment", "NewRecordMap"), transactionFilter, null, ContainerFilter.CurrentAndSubfolders).getRows(); if (expectedEventCount != null) { diff --git a/src/org/labkey/test/util/EscapeUtil.java b/src/org/labkey/test/util/EscapeUtil.java index ad88def79c..7870369cbd 100644 --- a/src/org/labkey/test/util/EscapeUtil.java +++ b/src/org/labkey/test/util/EscapeUtil.java @@ -77,6 +77,19 @@ static public String toJSONRow(Map row) return sb.toString(); } + static public String toJSONRow(List> rows) + { + StringBuilder sb = new StringBuilder(); + String sep = ""; + for (Map row : rows) + { + sb.append(sep).append(toJSONRow(row)); + sep = ","; + } + + return sb.toString(); + } + static public String jsString(String s) { if (s == null) diff --git a/src/org/labkey/test/util/query/QueryApiHelper.java b/src/org/labkey/test/util/query/QueryApiHelper.java index 36b967051b..2e0d3e63ef 100644 --- a/src/org/labkey/test/util/query/QueryApiHelper.java +++ b/src/org/labkey/test/util/query/QueryApiHelper.java @@ -12,6 +12,7 @@ import org.labkey.remoteapi.query.Filter; import org.labkey.remoteapi.query.ImportDataCommand; import org.labkey.remoteapi.query.ImportDataResponse; +import org.labkey.remoteapi.query.ImportExperimentDataCommand; import org.labkey.remoteapi.query.InsertRowsCommand; import org.labkey.remoteapi.query.MoveRowsCommand; import org.labkey.remoteapi.query.RowsResponse; @@ -21,6 +22,7 @@ import org.labkey.remoteapi.query.TruncateTableCommand; import org.labkey.remoteapi.query.TruncateTableResponse; import org.labkey.remoteapi.query.UpdateRowsCommand; +import org.labkey.test.util.AuditLogHelper; import java.io.File; import java.io.IOException; @@ -127,7 +129,20 @@ public ImportDataResponse importData(File file) throws IOException, CommandExcep ImportDataCommand importDataCommand = new ImportDataCommand(_schema, _query); importDataCommand.setFile(file); importDataCommand.setTimeout(_insertTimout); - return importDataCommand.execute(_connection, _containerPath); + return importDataCommand.execute(_connection, _containerPath); + } + + public ImportDataResponse importExperimentData(String text, AuditLogHelper.AuditBehaviorType auditBehaviorType, ImportDataCommand.InsertOption insertOption, boolean isCrossType, boolean isCrossFolder, boolean isAsync) throws IOException, CommandException + { + ImportExperimentDataCommand importDataCommand = new ImportExperimentDataCommand(_schema, _query, _containerPath); + importDataCommand.setAuditBehavior(auditBehaviorType); + importDataCommand.setUseAsync(isAsync); + importDataCommand.setCrossFolderImport(isCrossFolder); + importDataCommand.setCrossTypeImport(isCrossType); + importDataCommand.setText(text); + importDataCommand.setInsertOption(insertOption); + importDataCommand.setTimeout(_insertTimout); + return importDataCommand.execute(_connection, _containerPath); } /**