From fea8332976baef9a54162a097ecae621938e711e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernhard=20M=C3=BCller?= Date: Wed, 22 Jul 2026 09:21:25 +0200 Subject: [PATCH] refactor(mako,common): configurable topology delay, ThreadPool executor, event listener tests - SimulatedEdaTopologyService: topology delay configurable via eda.simulation.topology-delay-ms - Add AsyncConfig with ThreadPoolTaskExecutor (4 core, 8 max threads) - Remove @EnableAsync from EegPortalApplication (now in AsyncConfig) - Add topology-delay-ms and consent-delay-ms to application-dev.yml - Add MakoConsentListenerTest (4 tests) - Add MeteringPointEventListenerTest (5 tests) - Add NotificationListenerTest (8 tests) --- .../eeg/backend/EegPortalApplication.java | 2 - .../backend/common/config/AsyncConfig.java | 24 ++++ .../impl/SimulatedEdaTopologyService.java | 10 +- .../src/main/resources/application-dev.yml | 4 + .../listener/NotificationListenerTest.java | 132 ++++++++++++++++++ .../listener/MakoConsentListenerTest.java | 89 ++++++++++++ .../MeteringPointEventListenerTest.java | 113 +++++++++++++++ 7 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/AsyncConfig.java create mode 100644 eeg_backend/src/test/java/at/mueller/eeg/backend/common/event/listener/NotificationListenerTest.java create mode 100644 eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MakoConsentListenerTest.java create mode 100644 eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListenerTest.java diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java index 02d4e40..aee6b88 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/EegPortalApplication.java @@ -2,10 +2,8 @@ package at.mueller.eeg.backend; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication -@EnableAsync public class EegPortalApplication { public static void main(String[] args) { diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/AsyncConfig.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/AsyncConfig.java new file mode 100644 index 0000000..aa9b46f --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/config/AsyncConfig.java @@ -0,0 +1,24 @@ +package at.mueller.eeg.backend.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(4); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("eeg-async-"); + executor.initialize(); + return executor; + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/SimulatedEdaTopologyService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/SimulatedEdaTopologyService.java index d090103..8e79fd6 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/SimulatedEdaTopologyService.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/SimulatedEdaTopologyService.java @@ -3,6 +3,7 @@ package at.mueller.eeg.backend.mako.service.impl; import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse; import at.mueller.eeg.backend.mako.service.EdaTopologyService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @@ -11,11 +12,18 @@ import java.util.concurrent.TimeUnit; @Service public class SimulatedEdaTopologyService implements EdaTopologyService { + private final long topologyDelayMs; + + public SimulatedEdaTopologyService( + @Value("${eda.simulation.topology-delay-ms:10000}") long topologyDelayMs) { + this.topologyDelayMs = topologyDelayMs; + } + @Override public EdaTopologyResponse checkTopology(String atNumber) { log.info("Starte simulierte EDA-Topologieabfrage für Zählpunkt: {}", atNumber); try { - TimeUnit.SECONDS.sleep(10); + TimeUnit.MILLISECONDS.sleep(topologyDelayMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("EDA Simulation unterbrochen", e); diff --git a/eeg_backend/src/main/resources/application-dev.yml b/eeg_backend/src/main/resources/application-dev.yml index a72df3d..1b46e33 100644 --- a/eeg_backend/src/main/resources/application-dev.yml +++ b/eeg_backend/src/main/resources/application-dev.yml @@ -5,3 +5,7 @@ jwt: secret: dev-only-secret-do-not-use-in-production-2025 app: cors-origins: http://localhost:4200 +eda: + simulation: + consent-delay-ms: 3000 + topology-delay-ms: 1000 diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/common/event/listener/NotificationListenerTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/common/event/listener/NotificationListenerTest.java new file mode 100644 index 0000000..9720b5a --- /dev/null +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/common/event/listener/NotificationListenerTest.java @@ -0,0 +1,132 @@ +package at.mueller.eeg.backend.common.event.listener; + +import at.mueller.eeg.backend.common.domain.NotificationType; +import at.mueller.eeg.backend.common.event.*; +import at.mueller.eeg.backend.common.service.NotificationService; +import at.mueller.eeg.backend.community.domain.MeteringPoint; +import at.mueller.eeg.backend.community.domain.PointType; +import at.mueller.eeg.backend.community.domain.state.MakoState; +import at.mueller.eeg.backend.community.repository.MeteringPointRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NotificationListenerTest { + + @Mock + private NotificationService notificationService; + + @Mock + private MeteringPointRepository meteringPointRepository; + + @InjectMocks + private NotificationListener notificationListener; + + private UUID testUserId; + private String testAtNumber; + private MeteringPoint testPoint; + + @BeforeEach + void setUp() { + testUserId = UUID.randomUUID(); + testAtNumber = "AT1234567890123456789012345678901"; + + testPoint = new MeteringPoint(); + testPoint.setId(UUID.randomUUID()); + testPoint.setUserId(testUserId); + testPoint.setAtNumber(testAtNumber); + testPoint.setType(PointType.CONSUMER); + testPoint.setMakoState(MakoState.CONSENT_GRANTED); + } + + @Test + void handleUserApproved_createsSuccessNotification() { + notificationListener.handleUserApproved(new UserApprovedEvent(testUserId)); + + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(NotificationType.class); + verify(notificationService).create(eq(testUserId), anyString(), anyString(), typeCaptor.capture()); + assertEquals(NotificationType.SUCCESS, typeCaptor.getValue()); + } + + @Test + void handleUserRejected_createsErrorNotification() { + notificationListener.handleUserRejected(new UserRejectedEvent(testUserId)); + + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(NotificationType.class); + verify(notificationService).create(eq(testUserId), anyString(), anyString(), typeCaptor.capture()); + assertEquals(NotificationType.ERROR, typeCaptor.getValue()); + } + + @Test + void handleMakoConsentApproved_createsNotificationForMeteringPointOwner() { + when(meteringPointRepository.findByAtNumber(testAtNumber)).thenReturn(Optional.of(testPoint)); + + notificationListener.handleMakoConsentApproved(new MakoConsentApprovedEvent(testAtNumber)); + + verify(notificationService).create(eq(testUserId), anyString(), anyString(), eq(NotificationType.SUCCESS)); + } + + @Test + void handleMakoConsentRejected_createsErrorNotification() { + when(meteringPointRepository.findByAtNumber(testAtNumber)).thenReturn(Optional.of(testPoint)); + + notificationListener.handleMakoConsentRejected(new MakoConsentRejectedEvent(testAtNumber, "Abgelehnt")); + + verify(notificationService).create(eq(testUserId), anyString(), anyString(), eq(NotificationType.ERROR)); + } + + @Test + void handleEdaTopologyCompleted_eligible_createsSuccessNotification() { + notificationListener.handleEdaTopologyCompleted(new EdaTopologyCompletedEvent( + testUserId, testPoint.getId(), true, "Op", "UW", "T", "7" + )); + + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(NotificationType.class); + verify(notificationService).create(eq(testUserId), anyString(), anyString(), typeCaptor.capture()); + assertEquals(NotificationType.SUCCESS, typeCaptor.getValue()); + } + + @Test + void handleEdaTopologyCompleted_notEligible_createsWarningNotification() { + notificationListener.handleEdaTopologyCompleted(new EdaTopologyCompletedEvent( + testUserId, testPoint.getId(), false, null, null, null, null + )); + + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(NotificationType.class); + verify(notificationService).create(eq(testUserId), anyString(), anyString(), typeCaptor.capture()); + assertEquals(NotificationType.WARNING, typeCaptor.getValue()); + } + + @Test + void handleEdaTopologyFailed_createsErrorNotification() { + notificationListener.handleEdaTopologyFailed(new EdaTopologyFailedEvent( + testUserId, testPoint.getId(), "Timeout" + )); + + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(NotificationType.class); + verify(notificationService).create(eq(testUserId), anyString(), anyString(), typeCaptor.capture()); + assertEquals(NotificationType.ERROR, typeCaptor.getValue()); + } + + @Test + void handleMakoConsentApproved_pointNotFound_doesNotCreateNotification() { + when(meteringPointRepository.findByAtNumber(testAtNumber)).thenReturn(Optional.empty()); + + notificationListener.handleMakoConsentApproved(new MakoConsentApprovedEvent(testAtNumber)); + + verify(notificationService, never()).create(any(), anyString(), anyString(), any()); + } +} diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MakoConsentListenerTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MakoConsentListenerTest.java new file mode 100644 index 0000000..805ae52 --- /dev/null +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MakoConsentListenerTest.java @@ -0,0 +1,89 @@ +package at.mueller.eeg.backend.community.event.listener; + +import at.mueller.eeg.backend.common.event.EdaTopologyCheckEvent; +import at.mueller.eeg.backend.common.event.MakoConsentApprovedEvent; +import at.mueller.eeg.backend.common.event.MakoConsentRejectedEvent; +import at.mueller.eeg.backend.common.event.MakoConsentTechnicalFailedEvent; +import at.mueller.eeg.backend.community.domain.MeteringPoint; +import at.mueller.eeg.backend.community.domain.PointType; +import at.mueller.eeg.backend.community.domain.state.MakoState; +import at.mueller.eeg.backend.community.repository.MeteringPointRepository; +import org.junit.jupiter.api.BeforeEach; +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 org.springframework.context.ApplicationEventPublisher; + +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MakoConsentListenerTest { + + @Mock + private MeteringPointRepository meteringPointRepository; + + @Mock + private ApplicationEventPublisher eventPublisher; + + @InjectMocks + private MakoConsentListener makoConsentListener; + + private MeteringPoint testPoint; + private String testAtNumber; + + @BeforeEach + void setUp() { + testAtNumber = "AT1234567890123456789012345678901"; + testPoint = new MeteringPoint(); + testPoint.setId(UUID.randomUUID()); + testPoint.setUserId(UUID.randomUUID()); + testPoint.setAtNumber(testAtNumber); + testPoint.setType(PointType.CONSUMER); + testPoint.setMakoState(MakoState.WAITING_FOR_CONSENT); + } + + @Test + void handleMakoConsentApproved_triggersTopologyCheck() { + when(meteringPointRepository.findByAtNumber(testAtNumber)).thenReturn(Optional.of(testPoint)); + + makoConsentListener.handleMakoConsentApproved(new MakoConsentApprovedEvent(testAtNumber)); + + verify(meteringPointRepository).save(testPoint); + verify(eventPublisher).publishEvent(any(EdaTopologyCheckEvent.class)); + } + + @Test + void handleMakoConsentRejected_setsRejectedState() { + when(meteringPointRepository.findByAtNumber(testAtNumber)).thenReturn(Optional.of(testPoint)); + + makoConsentListener.handleMakoConsentRejected(new MakoConsentRejectedEvent(testAtNumber, "Kunde abgelehnt")); + + verify(meteringPointRepository).save(testPoint); + verify(eventPublisher, never()).publishEvent(any()); + } + + @Test + void handleMakoConsentTechnicalFailed_setsErrorState() { + when(meteringPointRepository.findByAtNumber(testAtNumber)).thenReturn(Optional.of(testPoint)); + + makoConsentListener.handleMakoConsentTechnicalFailed(new MakoConsentTechnicalFailedEvent(testAtNumber, "Timeout")); + + verify(meteringPointRepository).save(testPoint); + verify(eventPublisher, never()).publishEvent(any()); + } + + @Test + void handleMakoConsentApproved_pointNotFound_doesNotThrow() { + when(meteringPointRepository.findByAtNumber(testAtNumber)).thenReturn(Optional.empty()); + + makoConsentListener.handleMakoConsentApproved(new MakoConsentApprovedEvent(testAtNumber)); + + verify(meteringPointRepository, never()).save(any()); + } +} diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListenerTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListenerTest.java new file mode 100644 index 0000000..446d439 --- /dev/null +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/event/listener/MeteringPointEventListenerTest.java @@ -0,0 +1,113 @@ +package at.mueller.eeg.backend.community.event.listener; + +import at.mueller.eeg.backend.common.event.EdaTopologyCompletedEvent; +import at.mueller.eeg.backend.common.event.EdaTopologyFailedEvent; +import at.mueller.eeg.backend.common.event.UserRegisteredEvent; +import at.mueller.eeg.backend.common.event.UserRejectedEvent; +import at.mueller.eeg.backend.community.domain.MeteringPoint; +import at.mueller.eeg.backend.community.domain.PointType; +import at.mueller.eeg.backend.community.domain.state.MakoState; +import at.mueller.eeg.backend.community.domain.state.MakoTrigger; +import at.mueller.eeg.backend.community.repository.MeteringPointRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MeteringPointEventListenerTest { + + @Mock + private MeteringPointRepository meteringPointRepository; + + @Mock + private ApplicationEventPublisher eventPublisher; + + @InjectMocks + private MeteringPointEventListener listener; + + private MeteringPoint testPoint; + private UUID testPointId; + private UUID testUserId; + private String testAtNumber; + + @BeforeEach + void setUp() { + testPointId = UUID.randomUUID(); + testUserId = UUID.randomUUID(); + testAtNumber = "AT1234567890123456789012345678901"; + + testPoint = new MeteringPoint(); + testPoint.setId(testPointId); + testPoint.setUserId(testUserId); + testPoint.setAtNumber(testAtNumber); + testPoint.setType(PointType.CONSUMER); + testPoint.setMakoState(MakoState.CONSENT_GRANTED); + } + + @Test + void handleEdaTopologyCompleted_eligible_setsActiveState() { + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testPoint)); + + listener.handleEdaTopologyCompleted(new EdaTopologyCompletedEvent( + testUserId, testPointId, true, "Wiener Netze", "UW-01", "Trafo-01", "7" + )); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MeteringPoint.class); + verify(meteringPointRepository).save(captor.capture()); + assertEquals(MakoState.ACTIVE, captor.getValue().getMakoState()); + } + + @Test + void handleEdaTopologyCompleted_notEligible_setsRejectedState() { + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testPoint)); + + listener.handleEdaTopologyCompleted(new EdaTopologyCompletedEvent( + testUserId, testPointId, false, null, null, null, null + )); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MeteringPoint.class); + verify(meteringPointRepository).save(captor.capture()); + assertEquals(MakoState.REJECTED, captor.getValue().getMakoState()); + } + + @Test + void handleEdaTopologyFailed_setsErrorState() { + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testPoint)); + + listener.handleEdaTopologyFailed(new EdaTopologyFailedEvent(testUserId, testPointId, "Timeout")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MeteringPoint.class); + verify(meteringPointRepository).save(captor.capture()); + assertEquals(MakoState.ERROR, captor.getValue().getMakoState()); + } + + @Test + void handleEdaTopologyCompleted_pointNotFound_doesNotThrow() { + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.empty()); + + listener.handleEdaTopologyCompleted(new EdaTopologyCompletedEvent( + testUserId, testPointId, true, "Op", "UW", "T", "7" + )); + + verify(meteringPointRepository, never()).save(any()); + } + + @Test + void handleUserRejected_deletesMeteringPoints() { + listener.handleUserRejected(new UserRejectedEvent(testUserId)); + + verify(meteringPointRepository).deleteByUserId(testUserId); + } +}