automatic audits

This commit is contained in:
2023-02-15 16:06:06 -06:00
parent 3071c63857
commit 8924657fc1
22 changed files with 1148 additions and 27 deletions

View File

@ -165,9 +165,9 @@ class AuditActionTest extends BaseTest
Integer recordId2 = 1702;
Integer recordId3 = 1703;
AuditInput auditInput = new AuditInput();
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId1, Map.of(), "Test Audit", List.of("Detail1", "Detail2"));
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId1, Map.of(), "Test Audit", List.of(new QRecord().withValue("message", "Detail1"), new QRecord().withValue("message", "Detail2")));
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_ORDER, recordId2, Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, 47), "Test Another Audit", null);
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId3, Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, 42), "Audit 3", List.of("Detail3"));
AuditAction.appendToInput(auditInput, TestUtils.TABLE_NAME_PERSON_MEMORY, recordId3, Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, 42), "Audit 3", List.of(new QRecord().withValue("message", "Detail3")));
new AuditAction().execute(auditInput);
/////////////////////////////////////

View File

@ -0,0 +1,178 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.audits;
import java.time.Instant;
import java.util.List;
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.actions.audits.DMLAuditInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
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.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for DMLAuditAction
*******************************************************************************/
class DMLAuditActionTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QInstance qInstance = QContext.getQInstance();
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
List<QRecord> recordList = List.of(new QRecord().withValue("id", 1).withValue("firstName", "Darin").withValue("noOfShoes", 5).withValue("favoriteShapeId", null).withValue("createDate", Instant.now()).withValue("modifyDate", Instant.now()));
List<QRecord> oldRecordList = List.of(new QRecord().withValue("id", 1).withValue("firstName", "Tim").withValue("noOfShoes", null).withValue("favoriteShapeId", 1).withValue("lastName", "Simpson"));
////////////////////////////////////////////////////////
// set audit rules null - confirm no audits are built //
////////////////////////////////////////////////////////
{
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(null);
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordList));
List<QRecord> auditList = TestUtils.queryTable("audit");
assertTrue(auditList.isEmpty());
}
////////////////////////////////////////////////////////
// set audit level NONE - confirm no audits are built //
////////////////////////////////////////////////////////
{
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE));
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordList));
List<QRecord> auditList = TestUtils.queryTable("audit");
assertTrue(auditList.isEmpty());
}
/////////////////////////////////////////////////////////////
// set audit level RECORD - confirm only header, no detail //
/////////////////////////////////////////////////////////////
{
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.RECORD));
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(recordList));
List<QRecord> auditList = TestUtils.queryTable("audit");
assertEquals(1, auditList.size());
assertEquals("Record was Inserted", auditList.get(0).getValueString("message"));
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
assertTrue(auditDetailList.isEmpty());
MemoryRecordStore.getInstance().reset();
}
////////////////////////////////////////////////////////
// set audit level FIELD - confirm header, and detail //
////////////////////////////////////////////////////////
{
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(updateInput).withRecordList(recordList));
List<QRecord> auditList = TestUtils.queryTable("audit");
assertEquals(1, auditList.size());
assertEquals("Record was Edited", auditList.get(0).getValueString("message"));
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// since we didn't provide old-records, there should be a detail for every field in the updated record - OTHER THAN createDate and modifyDate //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertEquals(recordList.get(0).getValues().size() - 2, auditDetailList.size());
assertTrue(auditDetailList.stream().allMatch(r -> r.getValueString("message").matches("Set.*to.*")));
assertTrue(auditDetailList.stream().allMatch(r -> r.getValueString("fieldName") != null));
assertTrue(auditDetailList.stream().allMatch(r -> r.getValueString("oldValue") == null));
assertTrue(auditDetailList.stream().anyMatch(r -> r.getValueString("newValue") != null));
MemoryRecordStore.getInstance().reset();
}
//////////////////////////////////////////////
// this time supply old-records to the edit //
//////////////////////////////////////////////
{
qInstance.getTable(TestUtils.TABLE_NAME_SHAPE).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE));
TestUtils.insertDefaultShapes(qInstance);
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(updateInput).withOldRecordList(oldRecordList).withRecordList(recordList));
List<QRecord> auditList = TestUtils.queryTable("audit");
assertEquals(1, auditList.size());
assertEquals("Record was Edited", auditList.get(0).getValueString("message"));
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
assertEquals(3, auditDetailList.size());
assertEquals("Removed \"Triangle\" from Favorite Shape", auditDetailList.get(0).getValueString("message"));
assertEquals("favoriteShapeId", auditDetailList.get(0).getValueString("fieldName"));
assertEquals("Triangle", auditDetailList.get(0).getValueString("oldValue"));
assertNull(auditDetailList.get(0).getValueString("newValue"));
assertEquals("Changed First Name from \"Tim\" to \"Darin\"", auditDetailList.get(1).getValueString("message"));
assertEquals("firstName", auditDetailList.get(1).getValueString("fieldName"));
assertEquals("Tim", auditDetailList.get(1).getValueString("oldValue"));
assertEquals("Darin", auditDetailList.get(1).getValueString("newValue"));
assertEquals("Set No Of Shoes to 5", auditDetailList.get(2).getValueString("message"));
assertEquals("noOfShoes", auditDetailList.get(2).getValueString("fieldName"));
assertNull(auditDetailList.get(2).getValueString("oldValue"));
assertEquals("5", auditDetailList.get(2).getValueString("newValue"));
MemoryRecordStore.getInstance().reset();
}
/////////////////////////////////////////////////
// confirm we don't log null fields on inserts //
/////////////////////////////////////////////////
{
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(recordList));
List<QRecord> auditDetailList = TestUtils.queryTable("auditDetail");
assertFalse(auditDetailList.isEmpty());
assertTrue(auditDetailList.stream().noneMatch(r -> r.getValueString("message").contains("Favorite Shape")));
MemoryRecordStore.getInstance().reset();
}
}
}

View File

@ -24,11 +24,21 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.List;
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.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
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.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -79,4 +89,64 @@ class DeleteActionTest extends BaseTest
});
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuditsByPrimaryKey() throws QException
{
QInstance qInstance = QContext.getQInstance();
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1),
new QRecord().withValue("id", 2)));
new InsertAction().execute(insertInput);
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.RECORD));
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
deleteInput.setPrimaryKeys(List.of(1, 2));
new DeleteAction().execute(deleteInput);
List<QRecord> audits = TestUtils.queryTable("audit");
assertEquals(2, audits.size());
assertTrue(audits.stream().allMatch(r -> r.getValueString("message").equals("Record was Deleted")));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuditsByFilter() throws QException
{
QInstance qInstance = QContext.getQInstance();
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
insertInput.setRecords(List.of(
new QRecord().withValue("id", 1),
new QRecord().withValue("id", 2)));
new InsertAction().execute(insertInput);
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.RECORD));
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
deleteInput.setQueryFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, 1, 2)));
new DeleteAction().execute(deleteInput);
List<QRecord> audits = TestUtils.queryTable("audit");
assertEquals(2, audits.size());
assertTrue(audits.stream().allMatch(r -> r.getValueString("message").equals("Record was Deleted")));
}
}

View File

@ -83,8 +83,8 @@ class QValueFormatterTest extends BaseTest
assertEquals("No", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.BOOLEAN), false));
assertNull(QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), null));
assertEquals("5:00 AM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(5, 0)));
assertEquals("5:00 PM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(17, 0)));
assertEquals("5:00:00 AM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(5, 0)));
assertEquals("5:00:47 PM", QValueFormatter.formatValue(new QFieldMetaData().withType(QFieldType.TIME), LocalTime.of(17, 0, 47)));
//////////////////////////////////////////////////
// this one flows through the exceptional cases //
@ -196,8 +196,8 @@ class QValueFormatterTest extends BaseTest
void testFormatDates()
{
assertEquals("2023-02-01", QValueFormatter.formatDate(LocalDate.of(2023, Month.FEBRUARY, 1)));
assertEquals("2023-02-01 07:15 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15)));
assertEquals("2023-02-01 07:15 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15), ZoneId.of("US/Central"))));
assertEquals("2023-02-01 07:15:00 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15, 0)));
assertEquals("2023-02-01 07:15:47 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15, 47), ZoneId.of("US/Central"))));
}
}