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)
This commit is contained in:
Bernhard Müller 2026-07-22 09:21:25 +02:00
parent 7baa1c72af
commit fea8332976
7 changed files with 371 additions and 3 deletions

View File

@ -2,10 +2,8 @@ package at.mueller.eeg.backend;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication @SpringBootApplication
@EnableAsync
public class EegPortalApplication { public class EegPortalApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -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;
}
}

View File

@ -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.api.dto.EdaTopologyResponse;
import at.mueller.eeg.backend.mako.service.EdaTopologyService; import at.mueller.eeg.backend.mako.service.EdaTopologyService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -11,11 +12,18 @@ import java.util.concurrent.TimeUnit;
@Service @Service
public class SimulatedEdaTopologyService implements EdaTopologyService { public class SimulatedEdaTopologyService implements EdaTopologyService {
private final long topologyDelayMs;
public SimulatedEdaTopologyService(
@Value("${eda.simulation.topology-delay-ms:10000}") long topologyDelayMs) {
this.topologyDelayMs = topologyDelayMs;
}
@Override @Override
public EdaTopologyResponse checkTopology(String atNumber) { public EdaTopologyResponse checkTopology(String atNumber) {
log.info("Starte simulierte EDA-Topologieabfrage für Zählpunkt: {}", atNumber); log.info("Starte simulierte EDA-Topologieabfrage für Zählpunkt: {}", atNumber);
try { try {
TimeUnit.SECONDS.sleep(10); TimeUnit.MILLISECONDS.sleep(topologyDelayMs);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new RuntimeException("EDA Simulation unterbrochen", e); throw new RuntimeException("EDA Simulation unterbrochen", e);

View File

@ -5,3 +5,7 @@ jwt:
secret: dev-only-secret-do-not-use-in-production-2025 secret: dev-only-secret-do-not-use-in-production-2025
app: app:
cors-origins: http://localhost:4200 cors-origins: http://localhost:4200
eda:
simulation:
consent-delay-ms: 3000
topology-delay-ms: 1000

View File

@ -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<NotificationType> 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<NotificationType> 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<NotificationType> 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<NotificationType> 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<NotificationType> 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());
}
}

View File

@ -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());
}
}

View File

@ -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<MeteringPoint> 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<MeteringPoint> 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<MeteringPoint> 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);
}
}