Skip to content

Commit d80780a

Browse files
committed
test(audit): add integration tests for pluggable audit handlers
Tests full flow from DML operations through handler invocation: - Insert/Update/Delete trigger DML handlers with correct data - Processed handlers called after audit records created - Async handler execution - Multiple handlers support
1 parent 0176adb commit d80780a

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
/*
2+
* QQQ - Low-code Application Framework for Engineers.
3+
* Copyright (C) 2021-2025. Kingsrook, LLC
4+
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
5+
* contact@kingsrook.com
6+
* https://github.com/Kingsrook/
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
22+
package com.kingsrook.qqq.backend.core.actions.audits;
23+
24+
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.concurrent.CountDownLatch;
28+
import java.util.concurrent.TimeUnit;
29+
import com.kingsrook.qqq.backend.core.BaseTest;
30+
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
31+
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
32+
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
33+
import com.kingsrook.qqq.backend.core.context.QContext;
34+
import com.kingsrook.qqq.backend.core.exceptions.QException;
35+
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditHandlerInput;
36+
import com.kingsrook.qqq.backend.core.model.actions.audits.ProcessedAuditHandlerInput;
37+
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
38+
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
39+
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
40+
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
41+
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
42+
import com.kingsrook.qqq.backend.core.model.data.QRecord;
43+
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
44+
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditHandlerType;
45+
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
46+
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditHandlerMetaData;
47+
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
48+
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
49+
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
50+
import com.kingsrook.qqq.backend.core.utils.TestUtils;
51+
import org.junit.jupiter.api.AfterEach;
52+
import org.junit.jupiter.api.BeforeEach;
53+
import org.junit.jupiter.api.Test;
54+
import static org.assertj.core.api.Assertions.assertThat;
55+
import static org.junit.jupiter.api.Assertions.assertEquals;
56+
import static org.junit.jupiter.api.Assertions.assertNotNull;
57+
import static org.junit.jupiter.api.Assertions.assertTrue;
58+
59+
60+
/*******************************************************************************
61+
** Integration tests for audit handler system.
62+
** Tests the full flow from DML operations through to handler invocation.
63+
*******************************************************************************/
64+
class AuditHandlerIntegrationTest extends BaseTest
65+
{
66+
private static List<DMLAuditHandlerInput> capturedDMLInputs = new ArrayList<>();
67+
private static List<ProcessedAuditHandlerInput> capturedProcessedInputs = new ArrayList<>();
68+
private static CountDownLatch asyncLatch = null;
69+
70+
71+
72+
/*******************************************************************************
73+
** Reset captured data before each test
74+
*******************************************************************************/
75+
@BeforeEach
76+
void setUp() throws QException
77+
{
78+
capturedDMLInputs.clear();
79+
capturedProcessedInputs.clear();
80+
asyncLatch = null;
81+
82+
QInstance qInstance = QContext.getQInstance();
83+
new AuditsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
84+
85+
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
86+
.setAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD));
87+
}
88+
89+
90+
91+
/*******************************************************************************
92+
** Clean up memory store after each test
93+
*******************************************************************************/
94+
@AfterEach
95+
void tearDown()
96+
{
97+
MemoryRecordStore.getInstance().reset();
98+
}
99+
100+
101+
102+
/*******************************************************************************
103+
** Test that DML handler is called when performing an INSERT operation.
104+
*******************************************************************************/
105+
@Test
106+
void testDMLHandlerCalledOnInsert() throws QException
107+
{
108+
QInstance qInstance = QContext.getQInstance();
109+
qInstance.addAuditHandler(new QAuditHandlerMetaData()
110+
.withName("testDMLHandler")
111+
.withHandlerCode(new QCodeReference(CapturingDMLAuditHandler.class))
112+
.withHandlerType(AuditHandlerType.DML)
113+
.withIsAsync(false)
114+
.withEnabled(true));
115+
116+
InsertInput insertInput = new InsertInput();
117+
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
118+
insertInput.setRecords(List.of(
119+
new QRecord().withValue("firstName", "John").withValue("lastName", "Doe")
120+
));
121+
122+
InsertOutput insertOutput = new InsertAction().execute(insertInput);
123+
124+
assertEquals(1, capturedDMLInputs.size());
125+
DMLAuditHandlerInput captured = capturedDMLInputs.get(0);
126+
assertEquals(TestUtils.TABLE_NAME_PERSON_MEMORY, captured.getTableName());
127+
assertEquals(DMLAuditHandlerInput.DMLType.INSERT, captured.getDmlType());
128+
assertNotNull(captured.getNewRecords());
129+
assertEquals(1, captured.getNewRecords().size());
130+
assertEquals("John", captured.getNewRecords().get(0).getValueString("firstName"));
131+
assertNotNull(captured.getTimestamp());
132+
assertNotNull(captured.getSession());
133+
}
134+
135+
136+
137+
/*******************************************************************************
138+
** Test that DML handler is called when performing an UPDATE operation.
139+
*******************************************************************************/
140+
@Test
141+
void testDMLHandlerCalledOnUpdate() throws QException
142+
{
143+
QInstance qInstance = QContext.getQInstance();
144+
qInstance.addAuditHandler(new QAuditHandlerMetaData()
145+
.withName("testDMLHandler")
146+
.withHandlerCode(new QCodeReference(CapturingDMLAuditHandler.class))
147+
.withHandlerType(AuditHandlerType.DML)
148+
.withIsAsync(false)
149+
.withEnabled(true));
150+
151+
/////////////////////////
152+
// first insert a record
153+
/////////////////////////
154+
InsertInput insertInput = new InsertInput();
155+
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
156+
insertInput.setRecords(List.of(
157+
new QRecord().withValue("firstName", "John").withValue("lastName", "Doe")
158+
));
159+
InsertOutput insertOutput = new InsertAction().execute(insertInput);
160+
Integer insertedId = insertOutput.getRecords().get(0).getValueInteger("id");
161+
162+
capturedDMLInputs.clear();
163+
164+
/////////////////////////
165+
// now update the record
166+
/////////////////////////
167+
UpdateInput updateInput = new UpdateInput();
168+
updateInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
169+
updateInput.setRecords(List.of(
170+
new QRecord().withValue("id", insertedId).withValue("firstName", "Jane")
171+
));
172+
173+
new UpdateAction().execute(updateInput);
174+
175+
assertEquals(1, capturedDMLInputs.size());
176+
DMLAuditHandlerInput captured = capturedDMLInputs.get(0);
177+
assertEquals(TestUtils.TABLE_NAME_PERSON_MEMORY, captured.getTableName());
178+
assertEquals(DMLAuditHandlerInput.DMLType.UPDATE, captured.getDmlType());
179+
assertNotNull(captured.getNewRecords());
180+
assertEquals(1, captured.getNewRecords().size());
181+
assertEquals("Jane", captured.getNewRecords().get(0).getValueString("firstName"));
182+
183+
/////////////////////////////////////////////////////////////////////////
184+
// old records should be populated for updates (fetched by UpdateAction)
185+
/////////////////////////////////////////////////////////////////////////
186+
assertNotNull(captured.getOldRecords());
187+
assertEquals(1, captured.getOldRecords().size());
188+
}
189+
190+
191+
192+
/*******************************************************************************
193+
** Test that DML handler is called when performing a DELETE operation.
194+
*******************************************************************************/
195+
@Test
196+
void testDMLHandlerCalledOnDelete() throws QException
197+
{
198+
QInstance qInstance = QContext.getQInstance();
199+
qInstance.addAuditHandler(new QAuditHandlerMetaData()
200+
.withName("testDMLHandler")
201+
.withHandlerCode(new QCodeReference(CapturingDMLAuditHandler.class))
202+
.withHandlerType(AuditHandlerType.DML)
203+
.withIsAsync(false)
204+
.withEnabled(true));
205+
206+
/////////////////////////
207+
// first insert a record
208+
/////////////////////////
209+
InsertInput insertInput = new InsertInput();
210+
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
211+
insertInput.setRecords(List.of(
212+
new QRecord().withValue("firstName", "John").withValue("lastName", "Doe")
213+
));
214+
InsertOutput insertOutput = new InsertAction().execute(insertInput);
215+
Integer insertedId = insertOutput.getRecords().get(0).getValueInteger("id");
216+
217+
capturedDMLInputs.clear();
218+
219+
/////////////////////////
220+
// now delete the record
221+
/////////////////////////
222+
DeleteInput deleteInput = new DeleteInput();
223+
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
224+
deleteInput.setPrimaryKeys(List.of(insertedId));
225+
226+
new DeleteAction().execute(deleteInput);
227+
228+
assertEquals(1, capturedDMLInputs.size());
229+
DMLAuditHandlerInput captured = capturedDMLInputs.get(0);
230+
assertEquals(TestUtils.TABLE_NAME_PERSON_MEMORY, captured.getTableName());
231+
assertEquals(DMLAuditHandlerInput.DMLType.DELETE, captured.getDmlType());
232+
assertNotNull(captured.getNewRecords());
233+
assertEquals(1, captured.getNewRecords().size());
234+
}
235+
236+
237+
238+
/*******************************************************************************
239+
** Test that processed handler is called after audit records are inserted.
240+
*******************************************************************************/
241+
@Test
242+
void testProcessedHandlerCalledAfterAuditInsert() throws QException
243+
{
244+
QInstance qInstance = QContext.getQInstance();
245+
qInstance.addAuditHandler(new QAuditHandlerMetaData()
246+
.withName("testProcessedHandler")
247+
.withHandlerCode(new QCodeReference(CapturingProcessedAuditHandler.class))
248+
.withHandlerType(AuditHandlerType.PROCESSED)
249+
.withIsAsync(false)
250+
.withEnabled(true));
251+
252+
InsertInput insertInput = new InsertInput();
253+
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
254+
insertInput.setRecords(List.of(
255+
new QRecord().withValue("firstName", "John").withValue("lastName", "Doe")
256+
));
257+
258+
new InsertAction().execute(insertInput);
259+
260+
assertEquals(1, capturedProcessedInputs.size());
261+
ProcessedAuditHandlerInput captured = capturedProcessedInputs.get(0);
262+
assertNotNull(captured.getAuditSingleInputs());
263+
assertThat(captured.getAuditSingleInputs()).isNotEmpty();
264+
assertNotNull(captured.getTimestamp());
265+
assertNotNull(captured.getSession());
266+
}
267+
268+
269+
270+
/*******************************************************************************
271+
** Test that async DML handler is called asynchronously.
272+
*******************************************************************************/
273+
@Test
274+
void testAsyncDMLHandler() throws Exception
275+
{
276+
asyncLatch = new CountDownLatch(1);
277+
278+
QInstance qInstance = QContext.getQInstance();
279+
qInstance.addAuditHandler(new QAuditHandlerMetaData()
280+
.withName("testAsyncDMLHandler")
281+
.withHandlerCode(new QCodeReference(CapturingDMLAuditHandler.class))
282+
.withHandlerType(AuditHandlerType.DML)
283+
.withIsAsync(true)
284+
.withEnabled(true));
285+
286+
InsertInput insertInput = new InsertInput();
287+
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
288+
insertInput.setRecords(List.of(
289+
new QRecord().withValue("firstName", "Async").withValue("lastName", "Test")
290+
));
291+
292+
new InsertAction().execute(insertInput);
293+
294+
boolean completed = asyncLatch.await(5, TimeUnit.SECONDS);
295+
assertTrue(completed, "Async handler should complete within timeout");
296+
assertEquals(1, capturedDMLInputs.size());
297+
assertEquals("Async", capturedDMLInputs.get(0).getNewRecords().get(0).getValueString("firstName"));
298+
}
299+
300+
301+
302+
/*******************************************************************************
303+
** Test that multiple handlers are all called.
304+
*******************************************************************************/
305+
@Test
306+
void testMultipleHandlersCalled() throws QException
307+
{
308+
QInstance qInstance = QContext.getQInstance();
309+
310+
qInstance.addAuditHandler(new QAuditHandlerMetaData()
311+
.withName("dmlHandler")
312+
.withHandlerCode(new QCodeReference(CapturingDMLAuditHandler.class))
313+
.withHandlerType(AuditHandlerType.DML)
314+
.withIsAsync(false)
315+
.withEnabled(true));
316+
317+
qInstance.addAuditHandler(new QAuditHandlerMetaData()
318+
.withName("processedHandler")
319+
.withHandlerCode(new QCodeReference(CapturingProcessedAuditHandler.class))
320+
.withHandlerType(AuditHandlerType.PROCESSED)
321+
.withIsAsync(false)
322+
.withEnabled(true));
323+
324+
InsertInput insertInput = new InsertInput();
325+
insertInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
326+
insertInput.setRecords(List.of(
327+
new QRecord().withValue("firstName", "Multi").withValue("lastName", "Handler")
328+
));
329+
330+
new InsertAction().execute(insertInput);
331+
332+
assertEquals(1, capturedDMLInputs.size());
333+
assertEquals(1, capturedProcessedInputs.size());
334+
}
335+
336+
337+
338+
/*******************************************************************************
339+
** Capturing DML audit handler for testing.
340+
*******************************************************************************/
341+
public static class CapturingDMLAuditHandler implements DMLAuditHandlerInterface
342+
{
343+
@Override
344+
public String getName()
345+
{
346+
return "capturingDMLHandler";
347+
}
348+
349+
350+
@Override
351+
public void handleDMLAudit(DMLAuditHandlerInput input) throws QException
352+
{
353+
capturedDMLInputs.add(input);
354+
355+
if(asyncLatch != null)
356+
{
357+
asyncLatch.countDown();
358+
}
359+
}
360+
}
361+
362+
363+
364+
/*******************************************************************************
365+
** Capturing processed audit handler for testing.
366+
*******************************************************************************/
367+
public static class CapturingProcessedAuditHandler implements ProcessedAuditHandlerInterface
368+
{
369+
@Override
370+
public String getName()
371+
{
372+
return "capturingProcessedHandler";
373+
}
374+
375+
376+
@Override
377+
public void handleAudit(ProcessedAuditHandlerInput input) throws QException
378+
{
379+
capturedProcessedInputs.add(input);
380+
}
381+
}
382+
383+
}

0 commit comments

Comments
 (0)