From 4344fa472ad811992c92c1b47d7ff2e47e5eae21 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 19 Jan 2023 16:31:18 -0600 Subject: [PATCH] Initial checkin --- .../core/actions/audits/AuditAction.java | 227 +++++++++++++++++ .../core/model/actions/audits/AuditInput.java | 231 ++++++++++++++++++ .../model/actions/audits/AuditOutput.java | 33 +++ .../model/audits/AuditsMetaDataProvider.java | 173 +++++++++++++ .../core/actions/audits/AuditActionTest.java | 70 ++++++ 5 files changed, 734 insertions(+) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditInput.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditOutput.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java new file mode 100644 index 00000000..67213f58 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java @@ -0,0 +1,227 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.audits; + + +import java.io.Serializable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput; +import com.kingsrook.qqq.backend.core.model.actions.audits.AuditOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QUser; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; + + +/******************************************************************************* + ** Insert an audit (e.g., the same one) against 1 or more records. + ** + ** Takes care of managing the foreign key tables. + ** + ** Enforces that security key values are provided, if the table has any. + *******************************************************************************/ +public class AuditAction extends AbstractQActionFunction +{ + private static final QLogger LOG = QLogger.getLogger(AuditAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void execute(String tableName, Integer recordId, Map securityKeyValues, String message) + { + new AuditAction().execute(new AuditInput() + .withAuditTableName(tableName) + .withRecordIdList(List.of(recordId)) + .withSecurityKeyValues(securityKeyValues) + .withMessage(message)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public AuditOutput execute(AuditInput input) + { + AuditOutput auditOutput = new AuditOutput(); + try + { + QTableMetaData table = QContext.getQInstance().getTable(input.getAuditTableName()); + if(table == null) + { + throw (new QException("Requested audit for an unrecognized table name: " + input.getAuditTableName())); + } + + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + { + if(input.getSecurityKeyValues() == null || !input.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType())) + { + throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + input.getAuditTableName())); + } + } + + Integer auditTableId = getIdForName("auditTable", input.getAuditTableName()); + Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(input.getAuditUserName(), getSessionUserName())); + Instant timestamp = Objects.requireNonNullElse(input.getTimestamp(), Instant.now()); + + List auditRecords = new ArrayList<>(); + for(Integer recordId : input.getRecordIdList()) + { + QRecord record = new QRecord() + .withValue("auditTableId", auditTableId) + .withValue("auditUserId", auditUserId) + .withValue("timestamp", timestamp) + .withValue("message", input.getMessage()) + .withValue("recordId", recordId); + + if(input.getSecurityKeyValues() != null) + { + for(Map.Entry entry : input.getSecurityKeyValues().entrySet()) + { + record.setValue(entry.getKey(), entry.getValue()); + } + } + + auditRecords.add(record); + } + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName("audit"); + insertInput.setRecords(auditRecords); + new InsertAction().execute(insertInput); + } + catch(Exception e) + { + LOG.error("Error performing an audit", e); + } + return (auditOutput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getSessionUserName() + { + QUser user = QContext.getQSession().getUser(); + if(user == null) + { + return ("Unknown"); + } + return (user.getFullName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Integer getIdForName(String tableName, String nameValue) throws QException + { + Integer id = fetchIdFromName(tableName, nameValue); + if(id != null) + { + return id; + } + + try + { + LOG.debug("Inserting " + tableName + " named " + nameValue); + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(tableName); + QRecord record = new QRecord().withValue("name", nameValue); + + if(tableName.equals("auditTable")) + { + QTableMetaData table = QContext.getQInstance().getTable(nameValue); + if(table != null) + { + record.setValue("label", table.getLabel()); + } + } + + insertInput.setRecords(List.of(record)); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + id = insertOutput.getRecords().get(0).getValueInteger("id"); + if(id != null) + { + return id; + } + } + catch(Exception e) + { + //////////////////////////////////////////////////////////////////// + // assume this may mean a dupe-key - so - try another fetch below // + //////////////////////////////////////////////////////////////////// + LOG.debug("Caught error inserting " + tableName + " named " + nameValue + " - will try to re-fetch", e); + } + + id = fetchIdFromName(tableName, nameValue); + if(id != null) + { + return id; + } + + ///////////// + // give up // + ///////////// + throw (new QException("Unable to get id for " + tableName + " named " + nameValue)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Integer fetchIdFromName(String tableName, String nameValue) throws QException + { + GetInput getInput = new GetInput(); + getInput.setTableName(tableName); + getInput.setUniqueKey(Map.of("name", nameValue)); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() != null) + { + return (getOutput.getRecord().getValueInteger("id")); + } + return null; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditInput.java new file mode 100644 index 00000000..1e487f0d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditInput.java @@ -0,0 +1,231 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.actions.audits; + + +import java.io.Serializable; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AuditInput extends AbstractActionInput +{ + private String auditTableName; + private String auditUserName; + private Instant timestamp; + private String message; + private List recordIdList; + + private Map securityKeyValues; + + + + /******************************************************************************* + ** Getter for auditTableName + *******************************************************************************/ + public String getAuditTableName() + { + return (this.auditTableName); + } + + + + /******************************************************************************* + ** Setter for auditTableName + *******************************************************************************/ + public void setAuditTableName(String auditTableName) + { + this.auditTableName = auditTableName; + } + + + + /******************************************************************************* + ** Fluent setter for auditTableName + *******************************************************************************/ + public AuditInput withAuditTableName(String auditTableName) + { + this.auditTableName = auditTableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for auditUserName + *******************************************************************************/ + public String getAuditUserName() + { + return (this.auditUserName); + } + + + + /******************************************************************************* + ** Setter for auditUserName + *******************************************************************************/ + public void setAuditUserName(String auditUserName) + { + this.auditUserName = auditUserName; + } + + + + /******************************************************************************* + ** Fluent setter for auditUserName + *******************************************************************************/ + public AuditInput withAuditUserName(String auditUserName) + { + this.auditUserName = auditUserName; + return (this); + } + + + + /******************************************************************************* + ** Getter for timestamp + *******************************************************************************/ + public Instant getTimestamp() + { + return (this.timestamp); + } + + + + /******************************************************************************* + ** Setter for timestamp + *******************************************************************************/ + public void setTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + } + + + + /******************************************************************************* + ** Fluent setter for timestamp + *******************************************************************************/ + public AuditInput withTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for message + *******************************************************************************/ + public String getMessage() + { + return (this.message); + } + + + + /******************************************************************************* + ** Setter for message + *******************************************************************************/ + public void setMessage(String message) + { + this.message = message; + } + + + + /******************************************************************************* + ** Fluent setter for message + *******************************************************************************/ + public AuditInput withMessage(String message) + { + this.message = message; + return (this); + } + + + + /******************************************************************************* + ** Getter for recordIdList + *******************************************************************************/ + public List getRecordIdList() + { + return (this.recordIdList); + } + + + + /******************************************************************************* + ** Setter for recordIdList + *******************************************************************************/ + public void setRecordIdList(List recordIdList) + { + this.recordIdList = recordIdList; + } + + + + /******************************************************************************* + ** Fluent setter for recordIdList + *******************************************************************************/ + public AuditInput withRecordIdList(List recordIdList) + { + this.recordIdList = recordIdList; + return (this); + } + + + + /******************************************************************************* + ** Getter for securityKeyValues + *******************************************************************************/ + public Map getSecurityKeyValues() + { + return (this.securityKeyValues); + } + + + + /******************************************************************************* + ** Setter for securityKeyValues + *******************************************************************************/ + public void setSecurityKeyValues(Map securityKeyValues) + { + this.securityKeyValues = securityKeyValues; + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyValues + *******************************************************************************/ + public AuditInput withSecurityKeyValues(Map securityKeyValues) + { + this.securityKeyValues = securityKeyValues; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditOutput.java new file mode 100644 index 00000000..dc8ccefb --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditOutput.java @@ -0,0 +1,33 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.actions.audits; + + +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AuditOutput extends AbstractActionOutput +{ +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java new file mode 100644 index 00000000..fb1a0887 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/audits/AuditsMetaDataProvider.java @@ -0,0 +1,173 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.audits; + + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AuditsMetaDataProvider +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException + { + defineStandardAuditTables(instance, backendName, backendDetailEnricher); + defineStandardAuditPossibleValueSources(instance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineStandardAuditTables(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException + { + for(QTableMetaData tableMetaData : defineStandardAuditTables(backendName, backendDetailEnricher)) + { + instance.addTable(tableMetaData); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineStandardAuditPossibleValueSources(QInstance instance) + { + instance.addPossibleValueSource(new QPossibleValueSource() + .withName("auditTable") + .withTableName("auditTable") + ); + + instance.addPossibleValueSource(new QPossibleValueSource() + .withName("auditUser") + .withTableName("auditUser") + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List defineStandardAuditTables(String backendName, Consumer backendDetailEnricher) throws QException + { + List rs = new ArrayList<>(); + rs.add(enrich(backendDetailEnricher, defineAuditUserTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName))); + rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName))); + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData enrich(Consumer backendDetailEnricher, QTableMetaData table) + { + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(table); + } + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineAuditTableTable(String backendName) + { + return new QTableMetaData() + .withName("auditTable") + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withRecordLabelFields("label") + .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("name")) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withField(new QFieldMetaData("label", QFieldType.STRING)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineAuditUserTable(String backendName) + { + return new QTableMetaData() + .withName("auditUser") + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withRecordLabelFields("name") + .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("name")) + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineAuditTable(String backendName) + { + return new QTableMetaData() + .withName("audit") + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName("auditTable")) + .withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName("auditUser")) + .withField(new QFieldMetaData("recordId", QFieldType.INTEGER)) + .withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS)) + .withField(new QFieldMetaData("timestamp", QFieldType.DATE_TIME)); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java new file mode 100644 index 00000000..3d31715a --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/audits/AuditActionTest.java @@ -0,0 +1,70 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.audits; + + +import java.util.Map; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QUser; +import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for AuditAction + *******************************************************************************/ +class AuditActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + + String userName = "John Doe"; + QContext.init(qInstance, new QSession().withUser(new QUser().withFullName(userName))); + + Integer recordId = 1701; + AuditAction.execute(TestUtils.TABLE_NAME_PERSON_MEMORY, recordId, Map.of(), "Test Audit"); + + ///////////////////////////////////// + // make sure things can be fetched // + ///////////////////////////////////// + GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "auditTable", "name", TestUtils.TABLE_NAME_PERSON_MEMORY); + GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "auditUser", "name", userName); + QRecord auditRecord = GeneralProcessUtils.getRecordByFieldOrElseThrow(null, "audit", "recordId", recordId); + assertEquals("Test Audit", auditRecord.getValueString("message")); + } + +} \ No newline at end of file