diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..9437b7c --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,40 @@ +name: Test & Coverage +on: + pull_request: + branches: + - '*' + +jobs: + test-and-coverage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + + - name: Set up the Maven dependencies caching + uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Run tests + run: mvn --batch-mode --update-snapshots verify -Dgpg.skip=true + + - name: Add coverage + uses: madrapps/jacoco-report@v1.6.1 + with: + paths: | + ${{ github.workspace }}/**/target/site/jacoco/jacoco.xml + token: ${{ secrets.GITHUB_TOKEN }} + title: '### :zap: Coverage report' + update-comment: true + min-coverage-overall: 80 + min-coverage-changed-files: 60 + continue-on-error: false diff --git a/pom.xml b/pom.xml index e28858a..bf36dc4 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ 21 + 4.11.0 @@ -58,6 +59,18 @@ spring-kafka-test test + + org.springframework.boot + spring-boot-starter-test + test + + + org.mockito + mockito-core + ${mockito.version} + test + + @@ -122,9 +135,27 @@ false + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + test + + report + + + + - scm:git:https://github.com/sipios/spring-data-event.git scm:git:git@github.com:sipios/spring-data-event.git diff --git a/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java b/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java new file mode 100644 index 0000000..c43235a --- /dev/null +++ b/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java @@ -0,0 +1,113 @@ +package com.sipios.spring.data.event.broadcaster; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.CallbackException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.kafka.core.KafkaTemplate; + +public class DataEventBroadcasterTest { + private DataEventBroadcaster broadcaster; + private KafkaTemplate kafkaTemplate; + private ObjectMapper objectMapper; + + @BeforeEach + void beforeEach() { + kafkaTemplate = mock(KafkaTemplate.class); + objectMapper = new ObjectMapper(); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + } + + @ParameterizedTest + @CsvSource({ + "testEntity.created, testEntity.created", + "'', testentity.created" + }) + void testBroadcastEntityCreated(String topicLabel, String expectedTopic) throws Exception { + TestEntity entity = new TestEntity(1, "Test Name", true); + String expectedJson = objectMapper.writeValueAsString(entity); + + broadcaster.broadcastEntityCreated(entity, topicLabel); + + verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson)); + } + + @ParameterizedTest + @CsvSource({ + "testEntity.updated, testEntity.updated", + "'', testentity.updated" + }) + void testBroadcastEntityUpdated(String topicLabel, String expectedTopic) throws Exception { + TestEntity entity = new TestEntity(1, "Test Name", false); + String expectedJson = objectMapper.writeValueAsString(entity); + + broadcaster.broadcastEntityUpdated(entity, topicLabel); + + verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson)); + } + + @ParameterizedTest + @CsvSource({ + "testEntity.deleted, testEntity.deleted", + "'', testentity.deleted" + }) + void testBroadcastEntityDeleted(String topicLabel, String expectedTopic) throws Exception { + TestEntity entity = new TestEntity(1, "Test Name", true); + String expectedJson = objectMapper.writeValueAsString(entity); + + broadcaster.broadcastEntityDeleted(entity, topicLabel); + + verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson)); + } + + @Test + void testBroadcastEntityCreatedJsonProcessingException() throws Exception { + objectMapper = mock(ObjectMapper.class); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + TestEntity entity = new TestEntity(1, "Test Name", true); + + when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {}); + + assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityCreated(entity, "testEntity.created")); + } + + @Test + void testBroadcastEntityUpdatedJsonProcessingException() throws Exception { + objectMapper = mock(ObjectMapper.class); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + TestEntity entity = new TestEntity(1, "Test Name", false); + + when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {}); + + assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityUpdated(entity, "testEntity.updated")); + } + + @Test + void testBroadcastEntityDeletedJsonProcessingException() throws Exception { + objectMapper = mock(ObjectMapper.class); + broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper); + TestEntity entity = new TestEntity(1, "Test Name", true); + + when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {}); + + assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityDeleted(entity, "testEntity.deleted")); + } + + @Getter + @Setter + @AllArgsConstructor + public static class TestEntity { + private int id; + private String name; + private boolean active; + } +} diff --git a/src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java b/src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java new file mode 100644 index 0000000..e61c6de --- /dev/null +++ b/src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java @@ -0,0 +1,151 @@ +package com.sipios.spring.data.event.listener; + +import com.sipios.spring.data.event.annotation.DataEventEntity; +import com.sipios.spring.data.event.broadcaster.DataEventBroadcaster; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.event.spi.PostDeleteEvent; +import org.hibernate.event.spi.PostInsertEvent; +import org.hibernate.event.spi.PostUpdateEvent; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class DataEventListenerTest { + @Mock + private DataEventBroadcaster dataEventBroadcaster; + + @InjectMocks + private DataEventListener listener; + + @Nested + class AnnotatedEntityTests { + + @Test + public void testOnPostInsert() { + PostInsertEvent event = mock(PostInsertEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostInsert(event); + + verify(dataEventBroadcaster).broadcastEntityCreated(entity, "testEntity.created"); + } + + @Test + public void testOnPostUpdate() { + PostUpdateEvent event = mock(PostUpdateEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostUpdate(event); + + verify(dataEventBroadcaster).broadcastEntityUpdated(entity, "testEntity.updated"); + } + + @Test + public void testOnPostDelete() { + PostDeleteEvent event = mock(PostDeleteEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostDelete(event); + + verify(dataEventBroadcaster).broadcastEntityDeleted(entity, "testEntity.deleted"); + } + + @Test + public void testOnPostInsertJsonProcessingException() throws Exception { + PostInsertEvent event = mock(PostInsertEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityCreated(any(), any()); + + assertThrows(RuntimeException.class, () -> listener.onPostInsert(event)); + } + + @Test + public void testOnPostUpdateJsonProcessingException() throws Exception { + PostUpdateEvent event = mock(PostUpdateEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityUpdated(any(), any()); + + assertThrows(RuntimeException.class, () -> listener.onPostUpdate(event)); + } + + @Test + public void testOnPostDeleteJsonProcessingException() throws Exception { + PostDeleteEvent event = mock(PostDeleteEvent.class); + TestEntity entity = new TestEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityDeleted(any(), any()); + + assertThrows(RuntimeException.class, () -> listener.onPostDelete(event)); + } + + @DataEventEntity(creationTopic = "testEntity.created", updateTopic = "testEntity.updated", deletionTopic = "testEntity.deleted") + @Getter + @Setter + @AllArgsConstructor + private static class TestEntity { + private int id; + private String name; + private boolean active; + } + } + + @Nested + class NonAnnotatedEntityTests { + + @Test + public void testOnPostInsert() { + PostInsertEvent event = mock(PostInsertEvent.class); + NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostInsert(event); + + verify(dataEventBroadcaster, never()).broadcastEntityCreated(any(), any()); + } + + @Test + public void testOnPostUpdate() { + PostUpdateEvent event = mock(PostUpdateEvent.class); + NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostUpdate(event); + + verify(dataEventBroadcaster, never()).broadcastEntityUpdated(any(), any()); + } + + @Test + public void testOnPostDelete() { + PostDeleteEvent event = mock(PostDeleteEvent.class); + NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true); + when(event.getEntity()).thenReturn(entity); + + listener.onPostDelete(event); + + verify(dataEventBroadcaster, never()).broadcastEntityDeleted(any(), any()); + } + + @Getter + @Setter + @AllArgsConstructor + private static class NonAnnotatedEntity { + private int id; + private String name; + private boolean active; + } + } +}