From 0d95589d703b6b413aec4925b12fb0b32dade40d Mon Sep 17 00:00:00 2001 From: gmunozfe Date: Tue, 17 Sep 2024 15:12:49 +0200 Subject: [PATCH] Add test for coverage --- quarkus/addons/events/process/runtime/pom.xml | 11 + .../GroupingMessagingEventPublisher.java | 3 + .../GroupingMessagingEventPublisherTest.java | 386 ++++++++++++++++++ 3 files changed, 400 insertions(+) create mode 100644 quarkus/addons/events/process/runtime/src/test/java/org/kie/kogito/events/process/GroupingMessagingEventPublisherTest.java diff --git a/quarkus/addons/events/process/runtime/pom.xml b/quarkus/addons/events/process/runtime/pom.xml index dc5d417218d..50ef92684c1 100644 --- a/quarkus/addons/events/process/runtime/pom.xml +++ b/quarkus/addons/events/process/runtime/pom.xml @@ -78,6 +78,17 @@ org.slf4j slf4j-api + + + org.junit.jupiter + junit-jupiter + test + + + io.quarkus + quarkus-junit5-mockito + test + diff --git a/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java b/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java index 32939f4d482..61dbd523711 100644 --- a/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java +++ b/quarkus/addons/events/process/runtime/src/main/java/org/kie/kogito/events/process/GroupingMessagingEventPublisher.java @@ -44,6 +44,9 @@ public void publish(DataEvent event) { public void publish(Collection> events) { Map>> eventsByChannel = new HashMap<>(); for (DataEvent event : events) { + if (event == null) { + continue; + } getConsumer(event).ifPresent(c -> eventsByChannel.computeIfAbsent(c, k -> new ArrayList<>()).add(event)); } for (Entry>> item : eventsByChannel.entrySet()) { diff --git a/quarkus/addons/events/process/runtime/src/test/java/org/kie/kogito/events/process/GroupingMessagingEventPublisherTest.java b/quarkus/addons/events/process/runtime/src/test/java/org/kie/kogito/events/process/GroupingMessagingEventPublisherTest.java new file mode 100644 index 00000000000..dc77c171978 --- /dev/null +++ b/quarkus/addons/events/process/runtime/src/test/java/org/kie/kogito/events/process/GroupingMessagingEventPublisherTest.java @@ -0,0 +1,386 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.kie.kogito.events.process; + +import java.util.*; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kie.kogito.addon.quarkus.common.reactive.messaging.MessageDecoratorProvider; +import org.kie.kogito.event.DataEvent; +import org.kie.kogito.events.config.EventsRuntimeConfig; +import org.kie.kogito.events.process.AbstractMessagingEventPublisher.AbstractMessageEmitter; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.smallrye.reactive.messaging.MutinyEmitter; + +import jakarta.enterprise.inject.Instance; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@SuppressWarnings("unchecked") +public class GroupingMessagingEventPublisherTest { + + @Mock + private ObjectMapper json; + + @Mock + private MutinyEmitter processInstancesEventsEmitter; + + @Mock + private MutinyEmitter processDefinitionEventsEmitter; + + @Mock + private MutinyEmitter userTasksEventsEmitter; + + @Mock + private EventsRuntimeConfig eventsRuntimeConfig; + + @Mock + private MessageDecoratorProvider decoratorProvider; + + @Mock + private Message decoratedMessage; + + @Mock + private Instance decoratorProviderInstance; + + @Mock + private AbstractMessagingEventPublisher.AbstractMessageEmitter processInstanceConsumer; + + @Mock + private AbstractMessagingEventPublisher.AbstractMessageEmitter userTaskConsumer; + + @Mock + private AbstractMessagingEventPublisher.AbstractMessageEmitter processDefinitionConsumer; + + @Spy + @InjectMocks + private GroupingMessagingEventPublisher groupingMessagingEventPublisher; + + @Spy + @InjectMocks + private ReactiveMessagingEventPublisher reactiveMessagingEventPublisher; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + when(decoratorProviderInstance.isResolvable()).thenReturn(true); + when(decoratorProviderInstance.get()).thenReturn(decoratorProvider); + + when(eventsRuntimeConfig.isProcessInstancesPropagateError()).thenReturn(false); + when(eventsRuntimeConfig.isProcessDefinitionPropagateError()).thenReturn(false); + when(eventsRuntimeConfig.isUserTasksPropagateError()).thenReturn(false); + + when(eventsRuntimeConfig.isProcessInstancesEventsEnabled()).thenReturn(true); + when(eventsRuntimeConfig.isUserTasksEventsEnabled()).thenReturn(true); + } + + @Test + public void testGroupingMessagingEventPublisher_publish() throws Exception { + DataEvent event = mock(DataEvent.class); + when(event.getType()).thenReturn("ProcessInstanceErrorDataEvent"); + + // Test initialization + groupingMessagingEventPublisher.init(); + when(decoratorProvider.decorate(any(Message.class))).thenReturn(decoratedMessage); + + // Mock the message behavior + mockMessageForBothAckNack(decoratedMessage); + + // Call method + groupingMessagingEventPublisher.publish(event); + + // Verify that the consumer has been invoked + verify(processInstancesEventsEmitter).sendMessageAndForget(any()); + } + + @Test + public void testReactiveMessagingEventPublisher_publish() throws Exception { + DataEvent event = mock(DataEvent.class); + when(event.getType()).thenReturn("ProcessInstanceErrorDataEvent"); + + // Test initialization + reactiveMessagingEventPublisher.init(); + when(decoratorProvider.decorate(any(Message.class))).thenReturn(decoratedMessage); + + // Mock the message behavior + mockMessageForBothAckNack(decoratedMessage); + + // Call method + reactiveMessagingEventPublisher.publish(event); + + // Verify that the consumer has been invoked + verify(processInstancesEventsEmitter).sendMessageAndForget(any()); + } + + @Test + public void testPublishGroupingByChannel() { + // Create mock events + DataEvent processInstanceEvent = mock(DataEvent.class); + when(processInstanceEvent.getType()).thenReturn("ProcessInstanceStateDataEvent"); + + DataEvent userTaskEvent = mock(DataEvent.class); + when(userTaskEvent.getType()).thenReturn("UserTaskInstanceStateDataEvent"); + + // Mock getConsumer() to return different emitters based on event type + doReturn(Optional.of(processInstanceConsumer)).when(groupingMessagingEventPublisher).getConsumer(processInstanceEvent); + doReturn(Optional.of(userTaskConsumer)).when(groupingMessagingEventPublisher).getConsumer(userTaskEvent); + + // Create a collection of events with different types (ProcessInstance and UserTask) + Collection> events = Arrays.asList(processInstanceEvent, userTaskEvent); + + // Spy on the publisher's internal method to verify the calls + doNothing().when(groupingMessagingEventPublisher).publishToTopic(any(), any()); + + // Invoke the method to test + groupingMessagingEventPublisher.publish(events); + + // Capture and verify that the correct emitter was used for each event + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(processInstanceConsumer), anyCollection()); + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(userTaskConsumer), anyCollection()); + } + + @Test + public void testPublishMultipleEventsGroupedByChannel() { + // Create multiple events of different types + DataEvent processInstanceEvent1 = mock(DataEvent.class); + DataEvent processInstanceEvent2 = mock(DataEvent.class); + DataEvent userTaskEvent1 = mock(DataEvent.class); + DataEvent userTaskEvent2 = mock(DataEvent.class); + + when(processInstanceEvent1.getType()).thenReturn("ProcessInstanceStateDataEvent"); + when(processInstanceEvent2.getType()).thenReturn("ProcessInstanceStateDataEvent"); + when(userTaskEvent1.getType()).thenReturn("UserTaskInstanceStateDataEvent"); + when(userTaskEvent2.getType()).thenReturn("UserTaskInstanceStateDataEvent"); + + // Mock getConsumer() to return corresponding emitters for event types + doReturn(Optional.of(processInstanceConsumer)).when(groupingMessagingEventPublisher).getConsumer(processInstanceEvent1); + doReturn(Optional.of(processInstanceConsumer)).when(groupingMessagingEventPublisher).getConsumer(processInstanceEvent2); + doReturn(Optional.of(userTaskConsumer)).when(groupingMessagingEventPublisher).getConsumer(userTaskEvent1); + doReturn(Optional.of(userTaskConsumer)).when(groupingMessagingEventPublisher).getConsumer(userTaskEvent2); + + // Create a collection of events that would be grouped by channel + Collection> events = Arrays.asList(processInstanceEvent1, processInstanceEvent2, userTaskEvent1, userTaskEvent2); + + // Spy on the internal publishToTopic to verify grouping + doNothing().when(groupingMessagingEventPublisher).publishToTopic(any(), any()); + + // Invoke the method to test + groupingMessagingEventPublisher.publish(events); + + // Verify that two grouped publishToTopic calls are made: one for processInstanceConsumer, one for userTaskConsumer + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(processInstanceConsumer), anyCollection()); + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(userTaskConsumer), anyCollection()); + + // Verify that the right number of events was grouped and passed to each emitter + ArgumentCaptor>> captor = ArgumentCaptor.forClass(Collection.class); + + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(processInstanceConsumer), captor.capture()); + Collection> groupedProcessInstanceEvents = captor.getValue(); + assertEquals(2, groupedProcessInstanceEvents.size()); // both processInstanceEvents are grouped + + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(userTaskConsumer), captor.capture()); + Collection> groupedUserTaskEvents = captor.getValue(); + assertEquals(2, groupedUserTaskEvents.size()); // both userTaskEvents are grouped + } + + @Test + public void testPublishEmptyEventsCollection() { + Collection> events = Collections.emptyList(); + + // Spy on the internal publishToTopic to verify no calls are made + doNothing().when(groupingMessagingEventPublisher).publishToTopic(any(), any()); + + groupingMessagingEventPublisher.publish(events); + + // Verify that publishToTopic is never called + verify(groupingMessagingEventPublisher, never()).publishToTopic(any(), anyCollection()); + } + + @Test + public void testNoConsumersFound() { + DataEvent processInstanceEvent = mock(DataEvent.class); + when(processInstanceEvent.getType()).thenReturn("ProcessInstanceStateDataEvent"); + + DataEvent userTaskEvent = mock(DataEvent.class); + when(userTaskEvent.getType()).thenReturn("UserTaskInstanceStateDataEvent"); + + // Mock getConsumer() to return empty optionals (no consumers found) + doReturn(Optional.empty()).when(groupingMessagingEventPublisher).getConsumer(processInstanceEvent); + doReturn(Optional.empty()).when(groupingMessagingEventPublisher).getConsumer(userTaskEvent); + + // Create a collection of events + Collection> events = Arrays.asList(processInstanceEvent, userTaskEvent); + + // Spy on the publisher's internal method to verify no calls are made + doNothing().when(groupingMessagingEventPublisher).publishToTopic(any(), any()); + + // Invoke the method to test + groupingMessagingEventPublisher.publish(events); + + // Verify that publishToTopic is never called since no consumers were found + verify(groupingMessagingEventPublisher, never()).publishToTopic(any(), anyCollection()); + } + + @Test + void testPublishToTopic_ExceptionHandling() throws Exception { + DataEvent event = mock(DataEvent.class); + when(event.getType()).thenReturn("ProcessInstanceErrorDataEvent"); + + groupingMessagingEventPublisher.init(); + when(decoratorProvider.decorate(any(Message.class))).thenThrow(new RuntimeException("Serialization error")); + + // Mock the message behavior + mockMessageForBothAckNack(decoratedMessage); + + // Call method + groupingMessagingEventPublisher.publish(event); + + // Check that emitter.sendMessageAndForget was never called + verify(processInstancesEventsEmitter, never()).sendMessageAndForget(any()); + } + + @Test + public void testPublishUnsupportedEventType() { + DataEvent unsupportedEvent = mock(DataEvent.class); + when(unsupportedEvent.getType()).thenReturn("UnsupportedEvent"); + + doReturn(Optional.empty()).when(groupingMessagingEventPublisher).getConsumer(unsupportedEvent); + + Collection> events = Collections.singletonList(unsupportedEvent); + + groupingMessagingEventPublisher.publish(events); + + // Verify no publishing occurred since no consumer exists for unsupported event + verify(groupingMessagingEventPublisher, never()).publishToTopic(any(), anyCollection()); + } + + @Test + public void testEventsDisabledInConfig() { + DataEvent processInstanceEvent = mock(DataEvent.class); + when(processInstanceEvent.getType()).thenReturn("ProcessInstanceStateDataEvent"); + + DataEvent userTaskEvent = mock(DataEvent.class); + when(userTaskEvent.getType()).thenReturn("UserTaskInstanceStateDataEvent"); + + // Disable process and user task events in the config + when(eventsRuntimeConfig.isProcessInstancesEventsEnabled()).thenReturn(false); + when(eventsRuntimeConfig.isUserTasksEventsEnabled()).thenReturn(false); + + Collection> events = Arrays.asList(processInstanceEvent, userTaskEvent); + + groupingMessagingEventPublisher.publish(events); + + // Verify no publishing occurred since events are disabled + verify(groupingMessagingEventPublisher, never()).publishToTopic(any(), anyCollection()); + } + + @Test + public void testNullEventInCollection() { + DataEvent validEvent = mock(DataEvent.class); + when(validEvent.getType()).thenReturn("ProcessInstanceStateDataEvent"); + + Collection> events = Arrays.asList(validEvent, null); // One valid event and one null event + + // Return a mock consumer for the valid event + doReturn(Optional.of(processInstanceConsumer)).when(groupingMessagingEventPublisher).getConsumer(validEvent); + + // Call the method + groupingMessagingEventPublisher.publish(events); + + // Verify the valid event is processed + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(processInstanceConsumer), anyCollection()); + } + + @Test + public void testDecorateMessage() { + Message rawMessage = mock(Message.class); + when(decoratorProvider.decorate(rawMessage)).thenReturn(decoratedMessage); + + reactiveMessagingEventPublisher.init(); + + Message result = reactiveMessagingEventPublisher.decorateMessage(rawMessage); + assertEquals(decoratedMessage, result); + + verify(decoratorProvider).decorate(rawMessage); + } + + @Test + public void testPublishToTopicWithDecorator() throws Exception { + Object event = new Object(); + when(json.writeValueAsString(event)).thenReturn("eventString"); + + reactiveMessagingEventPublisher.init(); + + // Mock the message emitter + AbstractMessagingEventPublisher.AbstractMessageEmitter mockEmitter = mock(AbstractMessagingEventPublisher.AbstractMessageEmitter.class); + + // Ensure decorated message is used + when(decoratorProvider.decorate(any(Message.class))).thenReturn(decoratedMessage); + + // Spy on the reactiveMessagingEventPublisher to allow publishToTopic + reactiveMessagingEventPublisher.publishToTopic(mockEmitter, event); + + // Verify that the message was decorated and sent + verify(decoratorProvider).decorate(any(Message.class)); + verify(mockEmitter).accept(decoratedMessage); + } + + @Test + public void testPublishWithMultipleEventTypesSomeWithoutConsumers() { + DataEvent processInstanceEvent = mock(DataEvent.class); + when(processInstanceEvent.getType()).thenReturn("ProcessInstanceStateDataEvent"); + + DataEvent unsupportedEvent = mock(DataEvent.class); + when(unsupportedEvent.getType()).thenReturn("UnsupportedEvent"); + + doReturn(Optional.of(processInstanceConsumer)).when(groupingMessagingEventPublisher).getConsumer(processInstanceEvent); + doReturn(Optional.empty()).when(groupingMessagingEventPublisher).getConsumer(unsupportedEvent); + + Collection> events = Arrays.asList(processInstanceEvent, unsupportedEvent); + + groupingMessagingEventPublisher.publish(events); + + // Ensure that only the supported event was published + verify(groupingMessagingEventPublisher, times(1)).publishToTopic(eq(processInstanceConsumer), anyCollection()); + verify(groupingMessagingEventPublisher, never()).publishToTopic(any(), eq(Collections.singletonList(unsupportedEvent))); + } + + private void mockMessageForBothAckNack(Message message) { + when(message.withAck(any())).thenReturn(message); + when(message.withNack(any())).thenReturn(message); + } +}