From e26a9572ade40df4ca8d323ec3674f030bd96240 Mon Sep 17 00:00:00 2001 From: smetanka Date: Mon, 19 Jan 2026 14:47:40 +0100 Subject: [PATCH 1/6] [NAE-2355] CreateCaseEvent is published twice per creation - Removed duplicate publishEvent call in createCase method - Reordered addMessageToOutcome call for proper execution flow --- .../application/engine/workflow/service/WorkflowService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 218e6facfa..525a6543fd 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -347,14 +347,13 @@ public CreateCaseEventOutcome createCase(String netId, Function ma useCase = findOne(useCase.getStringId()); outcome.addOutcomes(eventService.runActions(petriNet.getPostCreateActions(), useCase, Optional.empty(), params)); useCase = findOne(useCase.getStringId()); + addMessageToOutcome(petriNet, CaseEventType.CREATE, outcome); useCase = evaluateRules(new CreateCaseEvent(new CreateCaseEventOutcome(useCase, outcome.getOutcomes()), EventPhase.POST)); // rulesExecuted = ruleEngine.evaluateRules(useCase, new CaseCreatedFact(useCase.getStringId(), EventPhase.POST)); // if (rulesExecuted > 0) { // useCase = save(useCase); // } outcome.setCase(setImmediateDataFields(useCase)); - addMessageToOutcome(petriNet, CaseEventType.CREATE, outcome); - publisher.publishEvent(new CreateCaseEvent(outcome, EventPhase.POST)); return outcome; } From 9227234d871fcca2ba69149ef148329e7637442c Mon Sep 17 00:00:00 2001 From: smetanka Date: Mon, 19 Jan 2026 14:47:40 +0100 Subject: [PATCH 2/6] [NAE-2355] CreateCaseEvent is published twice per creation - Removed duplicate publishEvent call in createCase method - Reordered addMessageToOutcome call for proper execution flow - Added testCreateCaseEventMultiplicity test --- .../workflow/service/WorkflowService.java | 3 +- .../application/engine/event/EventTest.java | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 218e6facfa..525a6543fd 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -347,14 +347,13 @@ public CreateCaseEventOutcome createCase(String netId, Function ma useCase = findOne(useCase.getStringId()); outcome.addOutcomes(eventService.runActions(petriNet.getPostCreateActions(), useCase, Optional.empty(), params)); useCase = findOne(useCase.getStringId()); + addMessageToOutcome(petriNet, CaseEventType.CREATE, outcome); useCase = evaluateRules(new CreateCaseEvent(new CreateCaseEventOutcome(useCase, outcome.getOutcomes()), EventPhase.POST)); // rulesExecuted = ruleEngine.evaluateRules(useCase, new CaseCreatedFact(useCase.getStringId(), EventPhase.POST)); // if (rulesExecuted > 0) { // useCase = save(useCase); // } outcome.setCase(setImmediateDataFields(useCase)); - addMessageToOutcome(petriNet, CaseEventType.CREATE, outcome); - publisher.publishEvent(new CreateCaseEvent(outcome, EventPhase.POST)); return outcome; } diff --git a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java new file mode 100644 index 0000000000..8178460482 --- /dev/null +++ b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java @@ -0,0 +1,92 @@ +package com.netgrif.application.engine.event; + + +import com.netgrif.application.engine.TestHelper; +import com.netgrif.application.engine.event.dispatchers.CaseDispatcher; +import com.netgrif.application.engine.objects.event.dispatchers.common.AbstractDispatcher; +import com.netgrif.application.engine.objects.event.events.workflow.CaseEvent; +import com.netgrif.application.engine.objects.event.events.workflow.CreateCaseEvent; +import com.netgrif.application.engine.objects.event.listeners.Listener; +import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; +import com.netgrif.application.engine.objects.petrinet.domain.events.EventPhase; +import com.netgrif.application.engine.startup.ImportHelper; +import com.netgrif.application.engine.startup.runner.SuperCreatorRunner; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.ArrayList; +import java.util.EventObject; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class EventTest { + + @Autowired + private TestHelper helper; + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private ImportHelper importHelper; + + @Autowired + private SuperCreatorRunner superCreator; + + @Autowired + private CaseDispatcher caseDispatcher; + + private PetriNet net; + + @BeforeEach + void beforeAll() { + helper.truncateDbs(); + Optional netOptional = importHelper.createNet("all_data.xml"); + assert netOptional.isPresent(); + this.net = netOptional.get(); + } + + + @Test + void testCreateCaseEventMultiplicity() { + List casesPreEvents = new ArrayList<>(); + List casesPostEvents = new ArrayList<>(); + Listener listener = new Listener() { + @Override + public void onEvent(E event, AbstractDispatcher dispatcher) { + + } + + @Override + public void onAsyncEvent(E event, AbstractDispatcher dispatcher) { + assertEquals(CreateCaseEvent.class, event.getClass()); + CreateCaseEvent createCaseEvent = (CreateCaseEvent) event; + assertNotNull(createCaseEvent); + assertNotNull(createCaseEvent.getEventPhase()); + if (createCaseEvent.getEventPhase()==EventPhase.PRE) { + casesPreEvents.add(createCaseEvent); + } else { + casesPostEvents.add(createCaseEvent); + } + + } + }; + listener.register(caseDispatcher, CreateCaseEvent.class, AbstractDispatcher.DispatchMethod.ASYNC); + workflowService.createCase(net.getStringId(), null, null, superCreator.getLoggedSuper()); + + assertEquals(1, casesPreEvents.size(), "Expected exactly one PRE phase event"); + assertEquals(1, casesPostEvents.size(), "Expected exactly one POST phase event"); + } +} From 31a7f1eed6ece1f1439558777ce9d634c9a5e924 Mon Sep 17 00:00:00 2001 From: smetanka Date: Tue, 20 Jan 2026 09:58:48 +0100 Subject: [PATCH 3/6] [NAE-2355] CreateCaseEvent is published twice per creation - Improve test for CreateCaseEvent publication multiplicity --- .../application/engine/event/EventTest.java | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java index 8178460482..414fded6d2 100644 --- a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java +++ b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java @@ -4,7 +4,6 @@ import com.netgrif.application.engine.TestHelper; import com.netgrif.application.engine.event.dispatchers.CaseDispatcher; import com.netgrif.application.engine.objects.event.dispatchers.common.AbstractDispatcher; -import com.netgrif.application.engine.objects.event.events.workflow.CaseEvent; import com.netgrif.application.engine.objects.event.events.workflow.CreateCaseEvent; import com.netgrif.application.engine.objects.event.listeners.Listener; import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; @@ -20,13 +19,14 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; -import java.util.ArrayList; import java.util.EventObject; -import java.util.List; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ActiveProfiles({"test"}) @@ -54,15 +54,19 @@ public class EventTest { void beforeAll() { helper.truncateDbs(); Optional netOptional = importHelper.createNet("all_data.xml"); - assert netOptional.isPresent(); + assertTrue(netOptional.isPresent()); this.net = netOptional.get(); } @Test void testCreateCaseEventMultiplicity() { - List casesPreEvents = new ArrayList<>(); - List casesPostEvents = new ArrayList<>(); + AtomicInteger preEventsCounter = new AtomicInteger(0); + AtomicInteger postEventsCounter = new AtomicInteger(0); + AtomicReference createCaseEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(2); + AtomicReference asyncException = new AtomicReference<>(); + Listener listener = new Listener() { @Override public void onEvent(E event, AbstractDispatcher dispatcher) { @@ -71,22 +75,41 @@ public void onEvent(E event, AbstractDispatcher dispatch @Override public void onAsyncEvent(E event, AbstractDispatcher dispatcher) { - assertEquals(CreateCaseEvent.class, event.getClass()); - CreateCaseEvent createCaseEvent = (CreateCaseEvent) event; - assertNotNull(createCaseEvent); - assertNotNull(createCaseEvent.getEventPhase()); - if (createCaseEvent.getEventPhase()==EventPhase.PRE) { - casesPreEvents.add(createCaseEvent); - } else { - casesPostEvents.add(createCaseEvent); + try { + createCaseEventRef.set(event); + CreateCaseEvent createCaseEvent = (CreateCaseEvent) event; + if (createCaseEvent.getEventPhase() == EventPhase.PRE) { + preEventsCounter.incrementAndGet(); + } else { + postEventsCounter.incrementAndGet(); + } + } catch (Throwable e) { + asyncException.set(e); + } finally { + latch.countDown(); } - } }; listener.register(caseDispatcher, CreateCaseEvent.class, AbstractDispatcher.DispatchMethod.ASYNC); workflowService.createCase(net.getStringId(), null, null, superCreator.getLoggedSuper()); - assertEquals(1, casesPreEvents.size(), "Expected exactly one PRE phase event"); - assertEquals(1, casesPostEvents.size(), "Expected exactly one POST phase event"); + boolean completed = false; + try { + completed = latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + fail("Interrupted while waiting for async events to complete"); + } + assertTrue(completed, "Async events did not complete within timeout"); + + if (asyncException.get() != null) { + fail("Exception in async event handler: " + asyncException.get().getMessage(), asyncException.get()); + } + + Object eventObj = createCaseEventRef.get(); + assertNotNull(eventObj, "Expected non-null event object"); + assertEquals(CreateCaseEvent.class, eventObj.getClass(), "Expected CreateCaseEvent class"); + assertNotNull(((CreateCaseEvent) eventObj).getEventPhase(), "Expected non-null Phase Enum"); + assertEquals(1, preEventsCounter.get(), "Expected exactly one PRE phase event"); + assertEquals(1, postEventsCounter.get(), "Expected exactly one POST phase event"); } } From 30799f241cbff460f626dd7529d30e9e166fc348 Mon Sep 17 00:00:00 2001 From: smetanka Date: Tue, 20 Jan 2026 09:58:48 +0100 Subject: [PATCH 4/6] [NAE-2355] CreateCaseEvent is published twice per creation - Improve test for CreateCaseEvent publication multiplicity - Fixed message passing to new CreateCaseEventOutcome --- .../workflow/service/WorkflowService.java | 4 +- .../application/engine/event/EventTest.java | 61 +++++++++++++------ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 525a6543fd..2a3adf5914 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -348,7 +348,9 @@ public CreateCaseEventOutcome createCase(String netId, Function ma outcome.addOutcomes(eventService.runActions(petriNet.getPostCreateActions(), useCase, Optional.empty(), params)); useCase = findOne(useCase.getStringId()); addMessageToOutcome(petriNet, CaseEventType.CREATE, outcome); - useCase = evaluateRules(new CreateCaseEvent(new CreateCaseEventOutcome(useCase, outcome.getOutcomes()), EventPhase.POST)); + CreateCaseEventOutcome eventOutcome = new CreateCaseEventOutcome(useCase, outcome.getOutcomes()); + eventOutcome.setMessage(outcome.getMessage()); + useCase = evaluateRules(new CreateCaseEvent(eventOutcome, EventPhase.POST)); // rulesExecuted = ruleEngine.evaluateRules(useCase, new CaseCreatedFact(useCase.getStringId(), EventPhase.POST)); // if (rulesExecuted > 0) { // useCase = save(useCase); diff --git a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java index 8178460482..414fded6d2 100644 --- a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java +++ b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java @@ -4,7 +4,6 @@ import com.netgrif.application.engine.TestHelper; import com.netgrif.application.engine.event.dispatchers.CaseDispatcher; import com.netgrif.application.engine.objects.event.dispatchers.common.AbstractDispatcher; -import com.netgrif.application.engine.objects.event.events.workflow.CaseEvent; import com.netgrif.application.engine.objects.event.events.workflow.CreateCaseEvent; import com.netgrif.application.engine.objects.event.listeners.Listener; import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; @@ -20,13 +19,14 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; -import java.util.ArrayList; import java.util.EventObject; -import java.util.List; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ActiveProfiles({"test"}) @@ -54,15 +54,19 @@ public class EventTest { void beforeAll() { helper.truncateDbs(); Optional netOptional = importHelper.createNet("all_data.xml"); - assert netOptional.isPresent(); + assertTrue(netOptional.isPresent()); this.net = netOptional.get(); } @Test void testCreateCaseEventMultiplicity() { - List casesPreEvents = new ArrayList<>(); - List casesPostEvents = new ArrayList<>(); + AtomicInteger preEventsCounter = new AtomicInteger(0); + AtomicInteger postEventsCounter = new AtomicInteger(0); + AtomicReference createCaseEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(2); + AtomicReference asyncException = new AtomicReference<>(); + Listener listener = new Listener() { @Override public void onEvent(E event, AbstractDispatcher dispatcher) { @@ -71,22 +75,41 @@ public void onEvent(E event, AbstractDispatcher dispatch @Override public void onAsyncEvent(E event, AbstractDispatcher dispatcher) { - assertEquals(CreateCaseEvent.class, event.getClass()); - CreateCaseEvent createCaseEvent = (CreateCaseEvent) event; - assertNotNull(createCaseEvent); - assertNotNull(createCaseEvent.getEventPhase()); - if (createCaseEvent.getEventPhase()==EventPhase.PRE) { - casesPreEvents.add(createCaseEvent); - } else { - casesPostEvents.add(createCaseEvent); + try { + createCaseEventRef.set(event); + CreateCaseEvent createCaseEvent = (CreateCaseEvent) event; + if (createCaseEvent.getEventPhase() == EventPhase.PRE) { + preEventsCounter.incrementAndGet(); + } else { + postEventsCounter.incrementAndGet(); + } + } catch (Throwable e) { + asyncException.set(e); + } finally { + latch.countDown(); } - } }; listener.register(caseDispatcher, CreateCaseEvent.class, AbstractDispatcher.DispatchMethod.ASYNC); workflowService.createCase(net.getStringId(), null, null, superCreator.getLoggedSuper()); - assertEquals(1, casesPreEvents.size(), "Expected exactly one PRE phase event"); - assertEquals(1, casesPostEvents.size(), "Expected exactly one POST phase event"); + boolean completed = false; + try { + completed = latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + fail("Interrupted while waiting for async events to complete"); + } + assertTrue(completed, "Async events did not complete within timeout"); + + if (asyncException.get() != null) { + fail("Exception in async event handler: " + asyncException.get().getMessage(), asyncException.get()); + } + + Object eventObj = createCaseEventRef.get(); + assertNotNull(eventObj, "Expected non-null event object"); + assertEquals(CreateCaseEvent.class, eventObj.getClass(), "Expected CreateCaseEvent class"); + assertNotNull(((CreateCaseEvent) eventObj).getEventPhase(), "Expected non-null Phase Enum"); + assertEquals(1, preEventsCounter.get(), "Expected exactly one PRE phase event"); + assertEquals(1, postEventsCounter.get(), "Expected exactly one POST phase event"); } } From e1574e66967a18f42748d6b79fb2894feed8771c Mon Sep 17 00:00:00 2001 From: smetanka Date: Tue, 20 Jan 2026 10:21:23 +0100 Subject: [PATCH 5/6] [NAE-2355] CreateCaseEvent is published twice per creation - Unregister listener after test execution --- .../java/com/netgrif/application/engine/event/EventTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java index 414fded6d2..459fe59321 100644 --- a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java +++ b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java @@ -86,6 +86,7 @@ public void onAsyncEvent(E event, AbstractDispatcher dis } catch (Throwable e) { asyncException.set(e); } finally { + latch.countDown(); } } @@ -111,5 +112,6 @@ public void onAsyncEvent(E event, AbstractDispatcher dis assertNotNull(((CreateCaseEvent) eventObj).getEventPhase(), "Expected non-null Phase Enum"); assertEquals(1, preEventsCounter.get(), "Expected exactly one PRE phase event"); assertEquals(1, postEventsCounter.get(), "Expected exactly one POST phase event"); + caseDispatcher.unregisterListener(listener, CreateCaseEvent.class, AbstractDispatcher.DispatchMethod.ASYNC); } } From 8408ff492309e7eaed2317ed07d8088ee779f556 Mon Sep 17 00:00:00 2001 From: smetanka Date: Tue, 20 Jan 2026 10:21:23 +0100 Subject: [PATCH 6/6] [NAE-2355] CreateCaseEvent is published twice per creation - Unregister listener after test execution - Renamed test from `beforeAll` to `beforeEach` to better represent its functionality --- .../java/com/netgrif/application/engine/event/EventTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java index 414fded6d2..e91c31378c 100644 --- a/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java +++ b/application-engine/src/test/java/com/netgrif/application/engine/event/EventTest.java @@ -51,7 +51,7 @@ public class EventTest { private PetriNet net; @BeforeEach - void beforeAll() { + void beforeEach() { helper.truncateDbs(); Optional netOptional = importHelper.createNet("all_data.xml"); assertTrue(netOptional.isPresent()); @@ -86,6 +86,7 @@ public void onAsyncEvent(E event, AbstractDispatcher dis } catch (Throwable e) { asyncException.set(e); } finally { + latch.countDown(); } } @@ -111,5 +112,6 @@ public void onAsyncEvent(E event, AbstractDispatcher dis assertNotNull(((CreateCaseEvent) eventObj).getEventPhase(), "Expected non-null Phase Enum"); assertEquals(1, preEventsCounter.get(), "Expected exactly one PRE phase event"); assertEquals(1, postEventsCounter.get(), "Expected exactly one POST phase event"); + caseDispatcher.unregisterListener(listener, CreateCaseEvent.class, AbstractDispatcher.DispatchMethod.ASYNC); } }