From 0ef4f94933b1c933bf8d7028f6753b989f0295dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernhard=20M=C3=BCller?= Date: Fri, 24 Jul 2026 15:27:36 +0200 Subject: [PATCH] fix(frontend): add error handling for dashboard overview metering data API The empty error callback in loadMeteringData() silently swallowed API failures, causing the chart to show 'Keine Daten' instead of the actual error. Added proper error logging and user-facing error message. Also added DashboardServiceTest for getMeteringDataOverview with scenarios for data, no metering points, empty time range, and multiple metering points. --- .../service/DashboardServiceTest.java | 146 ++++++++++++++---- .../dashboard-overview/dashboard-overview.ts | 6 +- 2 files changed, 125 insertions(+), 27 deletions(-) diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceTest.java index e9514f6..d1ef0f1 100644 --- a/eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceTest.java +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceTest.java @@ -1,21 +1,29 @@ package at.mueller.eeg.backend.dashboard.service; -import at.mueller.eeg.backend.community.domain.MembershipStatus; +import at.mueller.eeg.backend.community.domain.*; +import at.mueller.eeg.backend.community.domain.state.MakoState; import at.mueller.eeg.backend.community.repository.EnergyCommunityRepository; -import at.mueller.eeg.backend.community.repository.MembershipRepository; +import at.mueller.eeg.backend.community.repository.MeteringDataRepository; import at.mueller.eeg.backend.community.repository.MeteringPointRepository; +import at.mueller.eeg.backend.community.repository.MembershipRepository; import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto; import at.mueller.eeg.backend.iam.domain.RegistrationStatus; import at.mueller.eeg.backend.iam.repository.UserRepository; +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 java.util.UUID; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -23,45 +31,131 @@ class DashboardServiceTest { @Mock private EnergyCommunityRepository energyCommunityRepository; - @Mock private UserRepository userRepository; - @Mock private MembershipRepository membershipRepository; - @Mock private MeteringPointRepository meteringPointRepository; + @Mock + private MeteringDataRepository meteringDataRepository; @InjectMocks private DashboardService dashboardService; - @Test - void getAdminStats_returnsCorrectCounts() { - when(energyCommunityRepository.count()).thenReturn(5L); - when(userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true)).thenReturn(3L); - when(membershipRepository.countByStatus(MembershipStatus.PENDING)).thenReturn(2L); - when(membershipRepository.countByStatus(MembershipStatus.ACTIVE)).thenReturn(10L); + private UUID userId; + private UUID meteringPointId; + private MeteringPoint meteringPoint; - DashboardStatsDto.AdminStats stats = dashboardService.getAdminStats(); + @BeforeEach + void setUp() { + userId = UUID.randomUUID(); + meteringPointId = UUID.randomUUID(); - assertEquals(5, stats.totalCommunities()); - assertEquals(3, stats.pendingUsers()); - assertEquals(2, stats.pendingMemberships()); - assertEquals(10, stats.totalActiveMemberships()); + meteringPoint = new MeteringPoint(); + meteringPoint.setId(meteringPointId); + meteringPoint.setUserId(userId); + meteringPoint.setAtNumber("AT0010000000000000000000001234567"); + meteringPoint.setType(PointType.CONSUMER); + meteringPoint.setMakoState(MakoState.ACTIVE); } @Test - void getUserStats_returnsCorrectCounts() { - UUID userId = UUID.randomUUID(); - when(meteringPointRepository.countByUserId(userId)).thenReturn(2L); - when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.ACTIVE)).thenReturn(1L); - when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.PENDING)).thenReturn(1L); + void getMeteringDataOverview_withData_returnsTimeSeries() { + Instant from = Instant.parse("2026-01-01T00:00:00Z"); + Instant to = Instant.parse("2026-01-03T00:00:00Z"); - DashboardStatsDto.UserStats stats = dashboardService.getUserStats(userId); + when(meteringPointRepository.findAllByUserId(userId)).thenReturn(List.of(meteringPoint)); - assertEquals(2, stats.totalMeteringPoints()); - assertEquals(1, stats.activeMemberships()); - assertEquals(1, stats.pendingMemberships()); + ZoneId systemZone = ZoneId.systemDefault(); + List meteringData = List.of( + createMeteringData(meteringPointId, Instant.parse("2026-01-01T10:00:00Z"), 5.0), + createMeteringData(meteringPointId, Instant.parse("2026-01-02T10:00:00Z"), 3.0) + ); + when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(meteringPointId, from, to)) + .thenReturn(meteringData); + + DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to); + + assertEquals(1, overview.meteringPoints().size()); + assertEquals("AT0010000000000000000000001234567", overview.meteringPoints().get(0).atNumber()); + assertEquals(8.0, overview.meteringPoints().get(0).totalKwh(), 0.01); + assertEquals(2, overview.timeSeries().size()); + } + + @Test + void getMeteringDataOverview_noMeteringPoints_returnsEmptyOverview() { + Instant from = Instant.parse("2026-01-01T00:00:00Z"); + Instant to = Instant.parse("2026-01-03T00:00:00Z"); + + when(meteringPointRepository.findAllByUserId(userId)).thenReturn(Collections.emptyList()); + + DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to); + + assertTrue(overview.meteringPoints().isEmpty()); + assertTrue(overview.timeSeries().isEmpty()); + } + + @Test + void getMeteringDataOverview_noDataInTimeRange_returnsEmptyTimeSeries() { + Instant from = Instant.parse("2026-01-01T00:00:00Z"); + Instant to = Instant.parse("2026-01-03T00:00:00Z"); + + when(meteringPointRepository.findAllByUserId(userId)).thenReturn(List.of(meteringPoint)); + when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(meteringPointId, from, to)) + .thenReturn(Collections.emptyList()); + + DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to); + + assertEquals(1, overview.meteringPoints().size()); + assertEquals(0.0, overview.meteringPoints().get(0).totalKwh(), 0.01); + assertTrue(overview.timeSeries().isEmpty()); + } + + @Test + void getMeteringDataOverview_multipleMeteringPoints_aggregatesCorrectly() { + Instant from = Instant.parse("2026-06-01T00:00:00Z"); + Instant to = Instant.parse("2026-06-02T00:00:00Z"); + + MeteringPoint mp2 = new MeteringPoint(); + mp2.setId(UUID.randomUUID()); + mp2.setUserId(userId); + mp2.setAtNumber("AT0010000000000000000000001234568"); + mp2.setType(PointType.PRODUCER); + mp2.setMakoState(MakoState.ACTIVE); + + when(meteringPointRepository.findAllByUserId(userId)).thenReturn(List.of(meteringPoint, mp2)); + + List data1 = List.of(createMeteringData(meteringPointId, Instant.parse("2026-06-01T10:00:00Z"), 5.0)); + List data2 = List.of(createMeteringData(mp2.getId(), Instant.parse("2026-06-01T10:00:00Z"), 3.0)); + + when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(meteringPointId, from, to)) + .thenReturn(data1); + when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(mp2.getId(), from, to)) + .thenReturn(data2); + + DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to); + + assertEquals(2, overview.meteringPoints().size()); + assertEquals(1, overview.timeSeries().size()); + + DashboardStatsDto.TimeSeriesEntry entry = overview.timeSeries().get(0); + assertEquals(5.0, entry.values().get("AT0010000000000000000000001234567"), 0.01); + assertEquals(3.0, entry.values().get("AT0010000000000000000000001234568"), 0.01); + } + + private MeteringData createMeteringData(UUID meteringPointId, Instant intervalStart, double kwh) { + MeteringPoint mp = new MeteringPoint(); + mp.setId(meteringPointId); + + MeteringData data = new MeteringData(); + data.setMeteringPoint(mp); + data.setDataType(MeteringDataType.CONSUMPTION); + data.setIntervalStart(intervalStart); + data.setIntervalEnd(intervalStart.plusSeconds(3600)); + data.setKwh(kwh); + data.setSource(DataSource.EMAIL_XLSX); + data.setUploadId(UUID.randomUUID()); + return data; } } diff --git a/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.ts b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.ts index f1958cb..ac4d063 100644 --- a/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.ts +++ b/eeg_frontend/src/app/pages/dashboard-overview/dashboard-overview.ts @@ -65,7 +65,11 @@ export class DashboardOverview implements OnInit { this.meteringData.set(data); this.buildChartOptions(data); }, - error: () => {} + error: (err) => { + console.error('Fehler beim Laden der Messdaten:', err); + this.error.set('Fehler beim Laden der Messdaten.'); + this.isLoading.set(false); + } }); }