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.
This commit is contained in:
Bernhard Müller 2026-07-24 15:27:36 +02:00
parent cfe989c426
commit 0ef4f94933
2 changed files with 125 additions and 27 deletions

View File

@ -1,21 +1,29 @@
package at.mueller.eeg.backend.dashboard.service; 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.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.MeteringPointRepository;
import at.mueller.eeg.backend.community.repository.MembershipRepository;
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto; import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
import at.mueller.eeg.backend.iam.domain.RegistrationStatus; import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
import at.mueller.eeg.backend.iam.repository.UserRepository; 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.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; 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.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -23,45 +31,131 @@ class DashboardServiceTest {
@Mock @Mock
private EnergyCommunityRepository energyCommunityRepository; private EnergyCommunityRepository energyCommunityRepository;
@Mock @Mock
private UserRepository userRepository; private UserRepository userRepository;
@Mock @Mock
private MembershipRepository membershipRepository; private MembershipRepository membershipRepository;
@Mock @Mock
private MeteringPointRepository meteringPointRepository; private MeteringPointRepository meteringPointRepository;
@Mock
private MeteringDataRepository meteringDataRepository;
@InjectMocks @InjectMocks
private DashboardService dashboardService; private DashboardService dashboardService;
@Test private UUID userId;
void getAdminStats_returnsCorrectCounts() { private UUID meteringPointId;
when(energyCommunityRepository.count()).thenReturn(5L); private MeteringPoint meteringPoint;
when(userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true)).thenReturn(3L);
when(membershipRepository.countByStatus(MembershipStatus.PENDING)).thenReturn(2L);
when(membershipRepository.countByStatus(MembershipStatus.ACTIVE)).thenReturn(10L);
DashboardStatsDto.AdminStats stats = dashboardService.getAdminStats(); @BeforeEach
void setUp() {
userId = UUID.randomUUID();
meteringPointId = UUID.randomUUID();
assertEquals(5, stats.totalCommunities()); meteringPoint = new MeteringPoint();
assertEquals(3, stats.pendingUsers()); meteringPoint.setId(meteringPointId);
assertEquals(2, stats.pendingMemberships()); meteringPoint.setUserId(userId);
assertEquals(10, stats.totalActiveMemberships()); meteringPoint.setAtNumber("AT0010000000000000000000001234567");
meteringPoint.setType(PointType.CONSUMER);
meteringPoint.setMakoState(MakoState.ACTIVE);
} }
@Test @Test
void getUserStats_returnsCorrectCounts() { void getMeteringDataOverview_withData_returnsTimeSeries() {
UUID userId = UUID.randomUUID(); Instant from = Instant.parse("2026-01-01T00:00:00Z");
when(meteringPointRepository.countByUserId(userId)).thenReturn(2L); Instant to = Instant.parse("2026-01-03T00:00:00Z");
when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.ACTIVE)).thenReturn(1L);
when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.PENDING)).thenReturn(1L);
DashboardStatsDto.UserStats stats = dashboardService.getUserStats(userId); when(meteringPointRepository.findAllByUserId(userId)).thenReturn(List.of(meteringPoint));
assertEquals(2, stats.totalMeteringPoints()); ZoneId systemZone = ZoneId.systemDefault();
assertEquals(1, stats.activeMemberships()); List<MeteringData> meteringData = List.of(
assertEquals(1, stats.pendingMemberships()); 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<MeteringData> data1 = List.of(createMeteringData(meteringPointId, Instant.parse("2026-06-01T10:00:00Z"), 5.0));
List<MeteringData> 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;
} }
} }

View File

@ -65,7 +65,11 @@ export class DashboardOverview implements OnInit {
this.meteringData.set(data); this.meteringData.set(data);
this.buildChartOptions(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);
}
}); });
} }