feat(notification-service): adapt to Go backend /api/v1/rooms/high-co2 endpoint

Replace SensorReading model with HighCo2Room matching the Go API response
{room, is_high, co2}. Update AirQualityService to call /api/v1/rooms/high-co2.
Simplify scheduler: backend already filters alertable rooms, recovery detected
by absence from response. Adapt Telegram message and all unit tests (20 passing).
This commit is contained in:
khalil-bot
2026-06-01 12:03:57 +02:00
parent 8cba5f7710
commit c26896df02
7 changed files with 105 additions and 152 deletions

View File

@@ -0,0 +1,9 @@
package ch.hesso.pi.notification.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public record HighCo2Room(
String room,
@JsonProperty("is_high") boolean isHigh,
int co2
) {}

View File

@@ -1,13 +0,0 @@
package ch.hesso.pi.notification.model;
import java.time.Instant;
public record SensorReading(
String roomId,
String roomName,
int co2,
double temperature,
int humidity,
String windowState,
Instant timestamp
) {}

View File

@@ -2,7 +2,7 @@ package ch.hesso.pi.notification.scheduler;
import ch.hesso.pi.notification.config.NotificationProperties; import ch.hesso.pi.notification.config.NotificationProperties;
import ch.hesso.pi.notification.model.Co2Level; import ch.hesso.pi.notification.model.Co2Level;
import ch.hesso.pi.notification.model.SensorReading; import ch.hesso.pi.notification.model.HighCo2Room;
import ch.hesso.pi.notification.service.AirQualityService; import ch.hesso.pi.notification.service.AirQualityService;
import ch.hesso.pi.notification.service.TelegramNotificationService; import ch.hesso.pi.notification.service.TelegramNotificationService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -10,7 +10,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -20,17 +19,16 @@ public class AirQualityScheduler {
private static final Logger log = LoggerFactory.getLogger(AirQualityScheduler.class); private static final Logger log = LoggerFactory.getLogger(AirQualityScheduler.class);
private static final List<SensorReading> MOCK_READINGS = List.of( private static final List<HighCo2Room> MOCK_ROOMS = List.of(
new SensorReading("A1", "Salle A1", 2150, 23.5, 55, "closed", Instant.now()), new HighCo2Room("A1", true, 2150),
new SensorReading("A2", "Salle A2", 1520, 22.0, 48, "open", Instant.now()), new HighCo2Room("A2", true, 1520)
new SensorReading("B1", "Salle B1", 780, 21.0, 42, "closed", Instant.now())
); );
private final AirQualityService airQualityService; private final AirQualityService airQualityService;
private final TelegramNotificationService telegramService; private final TelegramNotificationService telegramService;
private final NotificationProperties props; private final NotificationProperties props;
// last alerted level per room — null means no alert is active for that room // last alerted level per room — cleared on recovery
private final Map<String, Co2Level> lastAlertedLevel = new ConcurrentHashMap<>(); private final Map<String, Co2Level> lastAlertedLevel = new ConcurrentHashMap<>();
public AirQualityScheduler(AirQualityService airQualityService, public AirQualityScheduler(AirQualityService airQualityService,
@@ -45,32 +43,37 @@ public class AirQualityScheduler {
initialDelayString = "5000") initialDelayString = "5000")
public void checkAirQuality() { public void checkAirQuality() {
boolean mockMode = props.getAirQuality().isMockMode(); boolean mockMode = props.getAirQuality().isMockMode();
log.debug("Polling air quality data… (mock={})", mockMode); log.debug("Polling high-CO₂ rooms… (mock={})", mockMode);
List<SensorReading> readings = mockMode List<HighCo2Room> rooms = mockMode
? MOCK_READINGS ? MOCK_ROOMS
: airQualityService.fetchLatestReadings(); : airQualityService.fetchHighCo2Rooms();
if (readings.isEmpty()) { // rooms not in the response have recovered — clear their deduplication state
log.warn("No readings returned — skipping this cycle"); if (!mockMode) {
var activeRooms = rooms.stream().map(HighCo2Room::room).collect(java.util.stream.Collectors.toSet());
lastAlertedLevel.keySet().removeIf(roomId -> {
if (!activeRooms.contains(roomId)) {
log.info("Room {} recovered — resetting alert state", roomId);
return true;
}
return false;
});
}
if (rooms.isEmpty()) {
log.debug("No high-CO₂ rooms — skipping this cycle");
return; return;
} }
for (SensorReading reading : readings) { for (HighCo2Room room : rooms) {
Co2Level level = airQualityService.resolveLevel(reading.co2()); Co2Level level = airQualityService.resolveLevel(room.co2());
Co2Level previous = lastAlertedLevel.get(room.room());
if (airQualityService.isAlertable(level)) { if (level != previous) {
Co2Level previous = lastAlertedLevel.get(reading.roomId()); log.info("Alert level changed: room={} {} -> {} ({} ppm)",
if (level != previous) { room.room(), previous, level, room.co2());
log.info("Alert level changed: room={} {} -> {}", reading.roomId(), previous, level); telegramService.sendAlert(room, level);
telegramService.sendAlert(reading, level); lastAlertedLevel.put(room.room(), level);
lastAlertedLevel.put(reading.roomId(), level);
}
} else {
// room recovered — reset so next alert triggers a new notification
if (lastAlertedLevel.remove(reading.roomId()) != null) {
log.info("Room {} recovered to {}", reading.roomId(), level);
}
} }
} }
} }

View File

@@ -2,7 +2,7 @@ package ch.hesso.pi.notification.service;
import ch.hesso.pi.notification.config.NotificationProperties; import ch.hesso.pi.notification.config.NotificationProperties;
import ch.hesso.pi.notification.model.Co2Level; import ch.hesso.pi.notification.model.Co2Level;
import ch.hesso.pi.notification.model.SensorReading; import ch.hesso.pi.notification.model.HighCo2Room;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
@@ -30,11 +30,11 @@ public class AirQualityService {
} }
/** /**
* Fetches the latest sensor reading for every room from the backend API. * Fetches rooms whose CO₂ exceeds 1400 ppm from GET /api/v1/rooms/high-co2.
* Returns an empty list on error so the scheduler can continue safely. * Returns an empty list on error so the scheduler can continue safely.
*/ */
public List<SensorReading> fetchLatestReadings() { public List<HighCo2Room> fetchHighCo2Rooms() {
String url = props.getAirQuality().getApiUrl() + "/sensors/latest"; String url = props.getAirQuality().getApiUrl() + "/api/v1/rooms/high-co2";
String username = props.getAirQuality().getApiUsername(); String username = props.getAirQuality().getApiUsername();
String password = props.getAirQuality().getApiPassword(); String password = props.getAirQuality().getApiPassword();
@@ -42,42 +42,27 @@ public class AirQualityService {
var request = restClient.get().uri(url); var request = restClient.get().uri(url);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) { if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
String encoded = Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); String encoded = Base64.getEncoder()
.encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
request = request.header(HttpHeaders.AUTHORIZATION, "Basic " + encoded); request = request.header(HttpHeaders.AUTHORIZATION, "Basic " + encoded);
} }
List<SensorReading> readings = request List<HighCo2Room> rooms = request
.retrieve() .retrieve()
.body(new ParameterizedTypeReference<>() {}); .body(new ParameterizedTypeReference<>() {});
return readings != null ? readings : List.of(); return rooms != null ? rooms : List.of();
} catch (RestClientException e) { } catch (RestClientException e) {
log.error("Failed to fetch latest readings from {}: {}", url, e.getMessage()); log.error("Failed to fetch high-CO₂ rooms from {}: {}", url, e.getMessage());
return List.of(); return List.of();
} }
} }
/** Resolves the CO₂ level enum from a raw ppm value using configured thresholds. */
public Co2Level resolveLevel(int ppm) { public Co2Level resolveLevel(int ppm) {
NotificationProperties.Thresholds t = props.getAirQuality().getThresholds(); NotificationProperties.Thresholds t = props.getAirQuality().getThresholds();
if (ppm < 800) return Co2Level.EXCELLENT; if (ppm < 800) return Co2Level.EXCELLENT;
if (ppm < t.getModerate()) return Co2Level.GOOD; if (ppm < t.getModerate()) return Co2Level.GOOD;
if (ppm < t.getPoor()) return Co2Level.MODERATE; if (ppm < t.getPoor()) return Co2Level.MODERATE;
if (ppm < t.getCritical()) return Co2Level.POOR; if (ppm < t.getCritical()) return Co2Level.POOR;
return Co2Level.CRITICAL; return Co2Level.CRITICAL;
} }
/** Returns true if the given level meets or exceeds the configured alert threshold. */
public boolean isAlertable(Co2Level level) {
Co2Level floor = parseAlertFromLevel(props.getAirQuality().getAlertFromLevel());
return level.ordinal() >= floor.ordinal();
}
private Co2Level parseAlertFromLevel(String value) {
try {
return Co2Level.valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
log.warn("Unknown alert-from-level '{}', defaulting to POOR", value);
return Co2Level.POOR;
}
}
} }

View File

@@ -2,7 +2,7 @@ package ch.hesso.pi.notification.service;
import ch.hesso.pi.notification.config.NotificationProperties; import ch.hesso.pi.notification.config.NotificationProperties;
import ch.hesso.pi.notification.model.Co2Level; import ch.hesso.pi.notification.model.Co2Level;
import ch.hesso.pi.notification.model.SensorReading; import ch.hesso.pi.notification.model.HighCo2Room;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -11,16 +11,12 @@ import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map; import java.util.Map;
@Service @Service
public class TelegramNotificationService { public class TelegramNotificationService {
private static final Logger log = LoggerFactory.getLogger(TelegramNotificationService.class); private static final Logger log = LoggerFactory.getLogger(TelegramNotificationService.class);
private static final DateTimeFormatter TIME_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
private final RestClient restClient; private final RestClient restClient;
private final NotificationProperties props; private final NotificationProperties props;
@@ -30,17 +26,17 @@ public class TelegramNotificationService {
this.props = props; this.props = props;
} }
public void sendAlert(SensorReading reading, Co2Level level) { public void sendAlert(HighCo2Room room, Co2Level level) {
String token = props.getTelegram().getBotToken(); String token = props.getTelegram().getBotToken();
String chatId = props.getTelegram().getChatId(); String chatId = props.getTelegram().getChatId();
if (!StringUtils.hasText(token) || !StringUtils.hasText(chatId)) { if (!StringUtils.hasText(token) || !StringUtils.hasText(chatId)) {
log.warn("Telegram credentials not configured — skipping alert for room {}", reading.roomId()); log.warn("Telegram credentials not configured — skipping alert for room {}", room.room());
return; return;
} }
String url = "https://api.telegram.org/bot" + token + "/sendMessage"; String url = "https://api.telegram.org/bot" + token + "/sendMessage";
String text = buildMessage(reading, level); String text = buildMessage(room, level);
try { try {
restClient.post() restClient.post()
@@ -50,32 +46,20 @@ public class TelegramNotificationService {
.retrieve() .retrieve()
.toBodilessEntity(); .toBodilessEntity();
log.info("Telegram alert sent for room {} — {} ({} ppm)", reading.roomId(), level.getLabel(), reading.co2()); log.info("Telegram alert sent for room {} — {} ({} ppm)", room.room(), level.getLabel(), room.co2());
} catch (RestClientException e) { } catch (RestClientException e) {
log.error("Failed to send Telegram alert for room {}: {}", reading.roomId(), e.getMessage()); log.error("Failed to send Telegram alert for room {}: {}", room.room(), e.getMessage());
} }
} }
private String buildMessage(SensorReading reading, Co2Level level) { private String buildMessage(HighCo2Room room, Co2Level level) {
String timestamp = reading.timestamp() != null
? TIME_FMT.format(reading.timestamp())
: "";
return String.format( return String.format(
"<b>%s — Air Quality Alert</b>\n\n" + "<b>%s — Air Quality Alert</b>\n\n" +
"<b>Room:</b> %s\n" + "<b>Room:</b> %s\n" +
"<b>CO₂:</b> %d ppm\n" + "<b>CO₂:</b> %d ppm",
"<b>Temperature:</b> %.1f °C\n" +
"<b>Humidity:</b> %d %%\n" +
"<b>Windows:</b> %s\n" +
"<b>Time:</b> %s",
level.getLabel(), level.getLabel(),
reading.roomName(), room.room(),
reading.co2(), room.co2()
reading.temperature(),
reading.humidity(),
reading.windowState(),
timestamp
); );
} }
} }

View File

@@ -2,7 +2,7 @@ package ch.hesso.pi.notification.service;
import ch.hesso.pi.notification.config.NotificationProperties; import ch.hesso.pi.notification.config.NotificationProperties;
import ch.hesso.pi.notification.model.Co2Level; import ch.hesso.pi.notification.model.Co2Level;
import ch.hesso.pi.notification.model.SensorReading; import ch.hesso.pi.notification.model.HighCo2Room;
import org.junit.jupiter.api.BeforeEach; 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;
@@ -14,7 +14,6 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
import java.time.Instant;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -38,7 +37,7 @@ class AirQualityServiceTest {
service = new AirQualityService(restClient, props); service = new AirQualityService(restClient, props);
} }
// ── resolveLevel ───────────────────────────────────────────────────────── // ── resolveLevel ─────────────────────────────────────────────────────────
@ParameterizedTest(name = "{0} ppm -> {1}") @ParameterizedTest(name = "{0} ppm -> {1}")
@CsvSource({ @CsvSource({
@@ -57,70 +56,61 @@ class AirQualityServiceTest {
assertThat(service.resolveLevel(ppm)).isEqualTo(expected); assertThat(service.resolveLevel(ppm)).isEqualTo(expected);
} }
// ── isAlertable ─────────────────────────────────────────────────────────── // ── fetchHighCo2Rooms ─────────────────────────────────────────────────────
@Test
void isAlertable_defaultThresholdIsPoor() {
assertThat(service.isAlertable(Co2Level.EXCELLENT)).isFalse();
assertThat(service.isAlertable(Co2Level.GOOD)).isFalse();
assertThat(service.isAlertable(Co2Level.MODERATE)).isFalse();
assertThat(service.isAlertable(Co2Level.POOR)).isTrue();
assertThat(service.isAlertable(Co2Level.CRITICAL)).isTrue();
}
@Test
void isAlertable_respectsConfiguredLevel() {
props.getAirQuality().setAlertFromLevel("moderate");
assertThat(service.isAlertable(Co2Level.GOOD)).isFalse();
assertThat(service.isAlertable(Co2Level.MODERATE)).isTrue();
assertThat(service.isAlertable(Co2Level.POOR)).isTrue();
}
@Test
void isAlertable_unknownLevelDefaultsToPoor() {
props.getAirQuality().setAlertFromLevel("invalid");
assertThat(service.isAlertable(Co2Level.MODERATE)).isFalse();
assertThat(service.isAlertable(Co2Level.POOR)).isTrue();
}
// ── fetchLatestReadings ───────────────────────────────────────────────────
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Test @Test
void fetchLatestReadings_returnsReadingsOnSuccess() { void fetchHighCo2Rooms_returnsRoomsOnSuccess() {
List<SensorReading> expected = List.of( List<HighCo2Room> expected = List.of(
new SensorReading("A1", "Salle A1", 1500, 22.0, 50, "closed", Instant.now()) new HighCo2Room("A2", true, 1718),
new HighCo2Room("A3", true, 1653)
); );
doReturn(uriSpec).when(restClient).get(); doReturn(uriSpec).when(restClient).get();
doReturn(uriSpec).when(uriSpec).uri("http://test-api/sensors/latest"); doReturn(uriSpec).when(uriSpec).uri("http://test-api/api/v1/rooms/high-co2");
doReturn(responseSpec).when(uriSpec).retrieve(); doReturn(responseSpec).when(uriSpec).retrieve();
doReturn(expected).when(responseSpec).body(any(ParameterizedTypeReference.class)); doReturn(expected).when(responseSpec).body(any(ParameterizedTypeReference.class));
assertThat(service.fetchLatestReadings()).isEqualTo(expected); assertThat(service.fetchHighCo2Rooms()).isEqualTo(expected);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Test @Test
void fetchLatestReadings_returnsEmptyListOnApiError() { void fetchHighCo2Rooms_returnsEmptyListOnApiError() {
doReturn(uriSpec).when(restClient).get(); doReturn(uriSpec).when(restClient).get();
doReturn(uriSpec).when(uriSpec).uri("http://test-api/sensors/latest"); doReturn(uriSpec).when(uriSpec).uri("http://test-api/api/v1/rooms/high-co2");
doReturn(responseSpec).when(uriSpec).retrieve(); doReturn(responseSpec).when(uriSpec).retrieve();
doThrow(new RestClientException("connection refused")).when(responseSpec).body(any(ParameterizedTypeReference.class)); doThrow(new RestClientException("connection refused"))
.when(responseSpec).body(any(ParameterizedTypeReference.class));
assertThat(service.fetchLatestReadings()).isEmpty(); assertThat(service.fetchHighCo2Rooms()).isEmpty();
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Test @Test
void fetchLatestReadings_returnsEmptyListWhenApiReturnsNull() { void fetchHighCo2Rooms_returnsEmptyListWhenApiReturnsNull() {
doReturn(uriSpec).when(restClient).get(); doReturn(uriSpec).when(restClient).get();
doReturn(uriSpec).when(uriSpec).uri("http://test-api/sensors/latest"); doReturn(uriSpec).when(uriSpec).uri("http://test-api/api/v1/rooms/high-co2");
doReturn(responseSpec).when(uriSpec).retrieve(); doReturn(responseSpec).when(uriSpec).retrieve();
doReturn(null).when(responseSpec).body(any(ParameterizedTypeReference.class)); doReturn(null).when(responseSpec).body(any(ParameterizedTypeReference.class));
assertThat(service.fetchLatestReadings()).isEmpty(); assertThat(service.fetchHighCo2Rooms()).isEmpty();
}
@SuppressWarnings("unchecked")
@Test
void fetchHighCo2Rooms_sendsBasicAuthWhenCredentialsConfigured() {
props.getAirQuality().setApiUsername("user");
props.getAirQuality().setApiPassword("pass");
doReturn(uriSpec).when(restClient).get();
doReturn(uriSpec).when(uriSpec).uri(anyString());
doReturn(uriSpec).when(uriSpec).header(anyString(), anyString());
doReturn(responseSpec).when(uriSpec).retrieve();
doReturn(List.of()).when(responseSpec).body(any(ParameterizedTypeReference.class));
service.fetchHighCo2Rooms();
verify(uriSpec).header(eq("Authorization"), any(String.class));
} }
} }

View File

@@ -2,7 +2,7 @@ package ch.hesso.pi.notification.service;
import ch.hesso.pi.notification.config.NotificationProperties; import ch.hesso.pi.notification.config.NotificationProperties;
import ch.hesso.pi.notification.model.Co2Level; import ch.hesso.pi.notification.model.Co2Level;
import ch.hesso.pi.notification.model.SensorReading; import ch.hesso.pi.notification.model.HighCo2Room;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -10,8 +10,6 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import java.time.Instant;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*; import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
@@ -21,9 +19,7 @@ class TelegramNotificationServiceTest {
private TelegramNotificationService service; private TelegramNotificationService service;
private NotificationProperties props; private NotificationProperties props;
private static final SensorReading READING = new SensorReading( private static final HighCo2Room ROOM = new HighCo2Room("A2", true, 1718);
"A1", "Salle A1", 1500, 22.5, 55, "closed", Instant.parse("2026-01-01T10:00:00Z")
);
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@@ -44,7 +40,7 @@ class TelegramNotificationServiceTest {
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andRespond(withSuccess()); .andRespond(withSuccess());
service.sendAlert(READING, Co2Level.POOR); service.sendAlert(ROOM, Co2Level.POOR);
server.verify(); server.verify();
} }
@@ -54,11 +50,11 @@ class TelegramNotificationServiceTest {
server.expect(requestTo("https://api.telegram.org/bottest-token/sendMessage")) server.expect(requestTo("https://api.telegram.org/bottest-token/sendMessage"))
.andExpect(jsonPath("$.chat_id").value("123456789")) .andExpect(jsonPath("$.chat_id").value("123456789"))
.andExpect(jsonPath("$.parse_mode").value("HTML")) .andExpect(jsonPath("$.parse_mode").value("HTML"))
.andExpect(jsonPath("$.text").value(org.hamcrest.Matchers.containsString("Salle A1"))) .andExpect(jsonPath("$.text").value(org.hamcrest.Matchers.containsString("A2")))
.andExpect(jsonPath("$.text").value(org.hamcrest.Matchers.containsString("1500"))) .andExpect(jsonPath("$.text").value(org.hamcrest.Matchers.containsString("1718")))
.andRespond(withSuccess()); .andRespond(withSuccess());
service.sendAlert(READING, Co2Level.POOR); service.sendAlert(ROOM, Co2Level.POOR);
server.verify(); server.verify();
} }
@@ -67,18 +63,18 @@ class TelegramNotificationServiceTest {
void sendAlert_skipsWhenTokenMissing() { void sendAlert_skipsWhenTokenMissing() {
props.getTelegram().setBotToken(""); props.getTelegram().setBotToken("");
service.sendAlert(READING, Co2Level.POOR); service.sendAlert(ROOM, Co2Level.POOR);
server.verify(); // expects no requests server.verify();
} }
@Test @Test
void sendAlert_skipsWhenChatIdMissing() { void sendAlert_skipsWhenChatIdMissing() {
props.getTelegram().setChatId(""); props.getTelegram().setChatId("");
service.sendAlert(READING, Co2Level.POOR); service.sendAlert(ROOM, Co2Level.POOR);
server.verify(); // expects no requests server.verify();
} }
@Test @Test
@@ -86,7 +82,6 @@ class TelegramNotificationServiceTest {
server.expect(requestTo("https://api.telegram.org/bottest-token/sendMessage")) server.expect(requestTo("https://api.telegram.org/bottest-token/sendMessage"))
.andRespond(withServerError()); .andRespond(withServerError());
// must not propagate the exception service.sendAlert(ROOM, Co2Level.CRITICAL);
service.sendAlert(READING, Co2Level.CRITICAL);
} }
} }