From 62f8cb1b1061ec7402c3fa66aa079631b41eb6aa Mon Sep 17 00:00:00 2001 From: Mpumlwana Lakhikhaya <220204594@mycput.ac.za> Date: Mon, 8 Jun 2026 03:19:01 +0200 Subject: [PATCH 1/6] Test: Add integration tests for AlertService threshold (Issue #33) --- tests/com/hpms/services/AlertServiceTest.java | 227 +++++++++++++++++- 1 file changed, 222 insertions(+), 5 deletions(-) diff --git a/tests/com/hpms/services/AlertServiceTest.java b/tests/com/hpms/services/AlertServiceTest.java index 028e0cf..cc8b2af 100644 --- a/tests/com/hpms/services/AlertServiceTest.java +++ b/tests/com/hpms/services/AlertServiceTest.java @@ -2,21 +2,238 @@ import com.hpms.domain.Alert; import com.hpms.repositories.inmemory.InMemoryAlertRepository; +import com.hpms.services.exceptions.BusinessRuleException; +import com.hpms.services.exceptions.ResourceNotFoundException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; + +/** + * AlertServiceThresholdTest — Integration Tests for AlertService + * + * Tests threshold validation, state transitions, and business rules. + * Relates to: Issue #33 — Write integration test for AlertService threshold + */ +class AlertServiceThresholdTest { + + private AlertService service; + + @BeforeEach + void setUp() { + service = new AlertService(new InMemoryAlertRepository()); + } + + // ── 1. Threshold / triggeredValue validation ────────────────────────── + + @Test + void createAlert_throwsWhenTriggeredValueIsZero() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 0, "WARNING") + ); + assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); + } + + @Test + void createAlert_throwsWhenTriggeredValueIsNegative() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", -10.0, "CRITICAL") + ); + assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); + } + + @Test + void createAlert_succeedsWithValidThreshold() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + assertNotNull(alert); + assertEquals(140.0, alert.getTriggeredValue()); + assertEquals("WARNING", alert.getSeverity()); + } + + @Test + void createAlert_succeedsWithMinimalPositiveThreshold() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 0.1, "WARNING" + ); + assertEquals(0.1, alert.getTriggeredValue()); + } + + @Test + void createAlert_succeedsWithHighThresholdValue() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 999.9, "CRITICAL" + ); + assertEquals(999.9, alert.getTriggeredValue()); + } + + // ── 2. Required field validation ────────────────────────────────────── + + @Test + void createAlert_throwsWhenVitalTypeIsNull() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), null, 120.0, "WARNING") + ); + assertTrue(ex.getMessage().contains("vitalType is required")); + } + + @Test + void createAlert_throwsWhenVitalTypeIsBlank() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), " ", 120.0, "WARNING") + ); + assertTrue(ex.getMessage().contains("vitalType is required")); + } + + @Test + void createAlert_throwsWhenSeverityIsNull() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, null) + ); + assertTrue(ex.getMessage().contains("severity is required")); + } -class AlertServiceTest { @Test - void acknowledgeOpenAlert() { - AlertService service = new AlertService(new InMemoryAlertRepository()); - Alert alert = service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING"); + void createAlert_throwsWhenSeverityIsBlank() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, "") + ); + assertTrue(ex.getMessage().contains("severity is required")); + } + + // ── 3. State transitions ────────────────────────────────────────────── + + @Test + void triggerAlert_changesStatusToTriggered() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 150.0, "CRITICAL" + ); + Alert triggered = service.triggerAlert(alert.getAlertId()); + assertEquals("TRIGGERED", triggered.getStatus()); + } + @Test + void acknowledgeAlert_afterTrigger_changesStatusToAcknowledged() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); service.triggerAlert(alert.getAlertId()); Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); + } + @Test + void closeAlert_changesStatusToClosed() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" + ); + service.triggerAlert(alert.getAlertId()); + service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + Alert closed = service.closeAlert(alert.getAlertId()); + assertEquals("CLOSED", closed.getStatus()); + } + + @Test + void fullAlertLifecycle_createdToClosedSuccessfully() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "temperature", 39.5, "WARNING" + ); + assertNotNull(alert.getAlertId()); + + Alert triggered = service.triggerAlert(alert.getAlertId()); + assertEquals("TRIGGERED", triggered.getStatus()); + + Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); + + Alert closed = service.closeAlert(alert.getAlertId()); + assertEquals("CLOSED", closed.getStatus()); + } + + // ── 4. Retrieval ────────────────────────────────────────────────────── + + @Test + void getAlertById_returnsCorrectAlert() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 130.0, "WARNING" + ); + Alert fetched = service.getAlertById(alert.getAlertId()); + assertEquals(alert.getAlertId(), fetched.getAlertId()); + } + + @Test + void getAlertById_throwsWhenNotFound() { + assertThrows(ResourceNotFoundException.class, () -> + service.getAlertById(UUID.randomUUID()) + ); + } + + @Test + void getAllAlerts_returnsAllCreatedAlerts() { + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING"); + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 180.0, "CRITICAL"); + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 88.0, "WARNING"); + + List alerts = service.getAllAlerts(); + assertEquals(3, alerts.size()); + } + + @Test + void getAllAlerts_returnsEmptyListWhenNoAlerts() { + List alerts = service.getAllAlerts(); + assertTrue(alerts.isEmpty()); + } + + // ── 5. Delete ───────────────────────────────────────────────────────── + + @Test + void deleteAlert_removesAlertFromRepository() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + service.deleteAlert(alert.getAlertId()); + assertThrows(ResourceNotFoundException.class, () -> + service.getAlertById(alert.getAlertId()) + ); + } + + @Test + void deleteAlert_throwsWhenAlertDoesNotExist() { + assertThrows(ResourceNotFoundException.class, () -> + service.deleteAlert(UUID.randomUUID()) + ); + } + + // ── 6. Multiple patients / isolation ───────────────────────────────── + + @Test + void multipleAlerts_forDifferentPatients_areStoredIndependently() { + UUID patient1 = UUID.randomUUID(); + UUID patient2 = UUID.randomUUID(); + + Alert alert1 = service.createAlert(patient1, UUID.randomUUID(), "heartRate", 140.0, "WARNING"); + Alert alert2 = service.createAlert(patient2, UUID.randomUUID(), "bloodPressure", 190.0, "CRITICAL"); + + assertNotEquals(alert1.getAlertId(), alert2.getAlertId()); + assertEquals(2, service.getAllAlerts().size()); + } + + @Test + void triggeringOneAlert_doesNotAffectOtherAlerts() { + Alert alert1 = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + Alert alert2 = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" + ); + + service.triggerAlert(alert1.getAlertId()); + + Alert fetched2 = service.getAlertById(alert2.getAlertId()); + assertNotEquals("TRIGGERED", fetched2.getStatus()); } } \ No newline at end of file From f5aaaed1e90d149298a1c709104ff55a1a30a6a8 Mon Sep 17 00:00:00 2001 From: Mpumlwana Lakhikhaya <220204594@mycput.ac.za> Date: Mon, 8 Jun 2026 03:24:14 +0200 Subject: [PATCH 2/6] updated --- tests/com/hpms/services/AlertServiceTest.java | 227 +----------------- 1 file changed, 5 insertions(+), 222 deletions(-) diff --git a/tests/com/hpms/services/AlertServiceTest.java b/tests/com/hpms/services/AlertServiceTest.java index cc8b2af..028e0cf 100644 --- a/tests/com/hpms/services/AlertServiceTest.java +++ b/tests/com/hpms/services/AlertServiceTest.java @@ -2,238 +2,21 @@ import com.hpms.domain.Alert; import com.hpms.repositories.inmemory.InMemoryAlertRepository; -import com.hpms.services.exceptions.BusinessRuleException; -import com.hpms.services.exceptions.ResourceNotFoundException; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.List; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.*; - -/** - * AlertServiceThresholdTest — Integration Tests for AlertService - * - * Tests threshold validation, state transitions, and business rules. - * Relates to: Issue #33 — Write integration test for AlertService threshold - */ -class AlertServiceThresholdTest { - - private AlertService service; - - @BeforeEach - void setUp() { - service = new AlertService(new InMemoryAlertRepository()); - } - - // ── 1. Threshold / triggeredValue validation ────────────────────────── - - @Test - void createAlert_throwsWhenTriggeredValueIsZero() { - BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 0, "WARNING") - ); - assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); - } - - @Test - void createAlert_throwsWhenTriggeredValueIsNegative() { - BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", -10.0, "CRITICAL") - ); - assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); - } - - @Test - void createAlert_succeedsWithValidThreshold() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" - ); - assertNotNull(alert); - assertEquals(140.0, alert.getTriggeredValue()); - assertEquals("WARNING", alert.getSeverity()); - } - - @Test - void createAlert_succeedsWithMinimalPositiveThreshold() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 0.1, "WARNING" - ); - assertEquals(0.1, alert.getTriggeredValue()); - } - - @Test - void createAlert_succeedsWithHighThresholdValue() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 999.9, "CRITICAL" - ); - assertEquals(999.9, alert.getTriggeredValue()); - } - - // ── 2. Required field validation ────────────────────────────────────── - - @Test - void createAlert_throwsWhenVitalTypeIsNull() { - BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), null, 120.0, "WARNING") - ); - assertTrue(ex.getMessage().contains("vitalType is required")); - } - - @Test - void createAlert_throwsWhenVitalTypeIsBlank() { - BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), " ", 120.0, "WARNING") - ); - assertTrue(ex.getMessage().contains("vitalType is required")); - } - - @Test - void createAlert_throwsWhenSeverityIsNull() { - BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, null) - ); - assertTrue(ex.getMessage().contains("severity is required")); - } +import static org.junit.jupiter.api.Assertions.assertEquals; +class AlertServiceTest { @Test - void createAlert_throwsWhenSeverityIsBlank() { - BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, "") - ); - assertTrue(ex.getMessage().contains("severity is required")); - } - - // ── 3. State transitions ────────────────────────────────────────────── - - @Test - void triggerAlert_changesStatusToTriggered() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 150.0, "CRITICAL" - ); - Alert triggered = service.triggerAlert(alert.getAlertId()); - assertEquals("TRIGGERED", triggered.getStatus()); - } + void acknowledgeOpenAlert() { + AlertService service = new AlertService(new InMemoryAlertRepository()); + Alert alert = service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING"); - @Test - void acknowledgeAlert_afterTrigger_changesStatusToAcknowledged() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" - ); service.triggerAlert(alert.getAlertId()); Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); - assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); - } - @Test - void closeAlert_changesStatusToClosed() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" - ); - service.triggerAlert(alert.getAlertId()); - service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); - Alert closed = service.closeAlert(alert.getAlertId()); - assertEquals("CLOSED", closed.getStatus()); - } - - @Test - void fullAlertLifecycle_createdToClosedSuccessfully() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "temperature", 39.5, "WARNING" - ); - assertNotNull(alert.getAlertId()); - - Alert triggered = service.triggerAlert(alert.getAlertId()); - assertEquals("TRIGGERED", triggered.getStatus()); - - Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); - - Alert closed = service.closeAlert(alert.getAlertId()); - assertEquals("CLOSED", closed.getStatus()); - } - - // ── 4. Retrieval ────────────────────────────────────────────────────── - - @Test - void getAlertById_returnsCorrectAlert() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 130.0, "WARNING" - ); - Alert fetched = service.getAlertById(alert.getAlertId()); - assertEquals(alert.getAlertId(), fetched.getAlertId()); - } - - @Test - void getAlertById_throwsWhenNotFound() { - assertThrows(ResourceNotFoundException.class, () -> - service.getAlertById(UUID.randomUUID()) - ); - } - - @Test - void getAllAlerts_returnsAllCreatedAlerts() { - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING"); - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 180.0, "CRITICAL"); - service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 88.0, "WARNING"); - - List alerts = service.getAllAlerts(); - assertEquals(3, alerts.size()); - } - - @Test - void getAllAlerts_returnsEmptyListWhenNoAlerts() { - List alerts = service.getAllAlerts(); - assertTrue(alerts.isEmpty()); - } - - // ── 5. Delete ───────────────────────────────────────────────────────── - - @Test - void deleteAlert_removesAlertFromRepository() { - Alert alert = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" - ); - service.deleteAlert(alert.getAlertId()); - assertThrows(ResourceNotFoundException.class, () -> - service.getAlertById(alert.getAlertId()) - ); - } - - @Test - void deleteAlert_throwsWhenAlertDoesNotExist() { - assertThrows(ResourceNotFoundException.class, () -> - service.deleteAlert(UUID.randomUUID()) - ); - } - - // ── 6. Multiple patients / isolation ───────────────────────────────── - - @Test - void multipleAlerts_forDifferentPatients_areStoredIndependently() { - UUID patient1 = UUID.randomUUID(); - UUID patient2 = UUID.randomUUID(); - - Alert alert1 = service.createAlert(patient1, UUID.randomUUID(), "heartRate", 140.0, "WARNING"); - Alert alert2 = service.createAlert(patient2, UUID.randomUUID(), "bloodPressure", 190.0, "CRITICAL"); - - assertNotEquals(alert1.getAlertId(), alert2.getAlertId()); - assertEquals(2, service.getAllAlerts().size()); - } - - @Test - void triggeringOneAlert_doesNotAffectOtherAlerts() { - Alert alert1 = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" - ); - Alert alert2 = service.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" - ); - - service.triggerAlert(alert1.getAlertId()); - - Alert fetched2 = service.getAlertById(alert2.getAlertId()); - assertNotEquals("TRIGGERED", fetched2.getStatus()); } } \ No newline at end of file From 1932521412731d2184fc09d0ae073eb0e3bb01be Mon Sep 17 00:00:00 2001 From: Mpumlwana Lakhikhaya <220204594@mycput.ac.za> Date: Mon, 8 Jun 2026 03:25:47 +0200 Subject: [PATCH 3/6] Test: Add integration tests for AlertService threshold (Issue #33) --- .../services/AlertServiceThresholdTest.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 tests/com/hpms/services/AlertServiceThresholdTest.java diff --git a/tests/com/hpms/services/AlertServiceThresholdTest.java b/tests/com/hpms/services/AlertServiceThresholdTest.java new file mode 100644 index 0000000..cc8b2af --- /dev/null +++ b/tests/com/hpms/services/AlertServiceThresholdTest.java @@ -0,0 +1,239 @@ +package com.hpms.services; + +import com.hpms.domain.Alert; +import com.hpms.repositories.inmemory.InMemoryAlertRepository; +import com.hpms.services.exceptions.BusinessRuleException; +import com.hpms.services.exceptions.ResourceNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * AlertServiceThresholdTest — Integration Tests for AlertService + * + * Tests threshold validation, state transitions, and business rules. + * Relates to: Issue #33 — Write integration test for AlertService threshold + */ +class AlertServiceThresholdTest { + + private AlertService service; + + @BeforeEach + void setUp() { + service = new AlertService(new InMemoryAlertRepository()); + } + + // ── 1. Threshold / triggeredValue validation ────────────────────────── + + @Test + void createAlert_throwsWhenTriggeredValueIsZero() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 0, "WARNING") + ); + assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); + } + + @Test + void createAlert_throwsWhenTriggeredValueIsNegative() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", -10.0, "CRITICAL") + ); + assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); + } + + @Test + void createAlert_succeedsWithValidThreshold() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + assertNotNull(alert); + assertEquals(140.0, alert.getTriggeredValue()); + assertEquals("WARNING", alert.getSeverity()); + } + + @Test + void createAlert_succeedsWithMinimalPositiveThreshold() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 0.1, "WARNING" + ); + assertEquals(0.1, alert.getTriggeredValue()); + } + + @Test + void createAlert_succeedsWithHighThresholdValue() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 999.9, "CRITICAL" + ); + assertEquals(999.9, alert.getTriggeredValue()); + } + + // ── 2. Required field validation ────────────────────────────────────── + + @Test + void createAlert_throwsWhenVitalTypeIsNull() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), null, 120.0, "WARNING") + ); + assertTrue(ex.getMessage().contains("vitalType is required")); + } + + @Test + void createAlert_throwsWhenVitalTypeIsBlank() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), " ", 120.0, "WARNING") + ); + assertTrue(ex.getMessage().contains("vitalType is required")); + } + + @Test + void createAlert_throwsWhenSeverityIsNull() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, null) + ); + assertTrue(ex.getMessage().contains("severity is required")); + } + + @Test + void createAlert_throwsWhenSeverityIsBlank() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, "") + ); + assertTrue(ex.getMessage().contains("severity is required")); + } + + // ── 3. State transitions ────────────────────────────────────────────── + + @Test + void triggerAlert_changesStatusToTriggered() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 150.0, "CRITICAL" + ); + Alert triggered = service.triggerAlert(alert.getAlertId()); + assertEquals("TRIGGERED", triggered.getStatus()); + } + + @Test + void acknowledgeAlert_afterTrigger_changesStatusToAcknowledged() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + service.triggerAlert(alert.getAlertId()); + Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); + } + + @Test + void closeAlert_changesStatusToClosed() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" + ); + service.triggerAlert(alert.getAlertId()); + service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + Alert closed = service.closeAlert(alert.getAlertId()); + assertEquals("CLOSED", closed.getStatus()); + } + + @Test + void fullAlertLifecycle_createdToClosedSuccessfully() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "temperature", 39.5, "WARNING" + ); + assertNotNull(alert.getAlertId()); + + Alert triggered = service.triggerAlert(alert.getAlertId()); + assertEquals("TRIGGERED", triggered.getStatus()); + + Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); + + Alert closed = service.closeAlert(alert.getAlertId()); + assertEquals("CLOSED", closed.getStatus()); + } + + // ── 4. Retrieval ────────────────────────────────────────────────────── + + @Test + void getAlertById_returnsCorrectAlert() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 130.0, "WARNING" + ); + Alert fetched = service.getAlertById(alert.getAlertId()); + assertEquals(alert.getAlertId(), fetched.getAlertId()); + } + + @Test + void getAlertById_throwsWhenNotFound() { + assertThrows(ResourceNotFoundException.class, () -> + service.getAlertById(UUID.randomUUID()) + ); + } + + @Test + void getAllAlerts_returnsAllCreatedAlerts() { + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING"); + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 180.0, "CRITICAL"); + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 88.0, "WARNING"); + + List alerts = service.getAllAlerts(); + assertEquals(3, alerts.size()); + } + + @Test + void getAllAlerts_returnsEmptyListWhenNoAlerts() { + List alerts = service.getAllAlerts(); + assertTrue(alerts.isEmpty()); + } + + // ── 5. Delete ───────────────────────────────────────────────────────── + + @Test + void deleteAlert_removesAlertFromRepository() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + service.deleteAlert(alert.getAlertId()); + assertThrows(ResourceNotFoundException.class, () -> + service.getAlertById(alert.getAlertId()) + ); + } + + @Test + void deleteAlert_throwsWhenAlertDoesNotExist() { + assertThrows(ResourceNotFoundException.class, () -> + service.deleteAlert(UUID.randomUUID()) + ); + } + + // ── 6. Multiple patients / isolation ───────────────────────────────── + + @Test + void multipleAlerts_forDifferentPatients_areStoredIndependently() { + UUID patient1 = UUID.randomUUID(); + UUID patient2 = UUID.randomUUID(); + + Alert alert1 = service.createAlert(patient1, UUID.randomUUID(), "heartRate", 140.0, "WARNING"); + Alert alert2 = service.createAlert(patient2, UUID.randomUUID(), "bloodPressure", 190.0, "CRITICAL"); + + assertNotEquals(alert1.getAlertId(), alert2.getAlertId()); + assertEquals(2, service.getAllAlerts().size()); + } + + @Test + void triggeringOneAlert_doesNotAffectOtherAlerts() { + Alert alert1 = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + Alert alert2 = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" + ); + + service.triggerAlert(alert1.getAlertId()); + + Alert fetched2 = service.getAlertById(alert2.getAlertId()); + assertNotEquals("TRIGGERED", fetched2.getStatus()); + } +} \ No newline at end of file From 7ffd024331059d03e86c5df2b49f5e6d2e0790f7 Mon Sep 17 00:00:00 2001 From: Mpumlwana Lakhikhaya <220204594@mycput.ac.za> Date: Mon, 8 Jun 2026 03:56:42 +0200 Subject: [PATCH 4/6] Feature: Add email notification service for critical alerts (Issue #7) --- .../services/EmailNotificationService.java | 163 ++++++++++++++ .../EmailNotificationServiceTest.java | 213 ++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 services/com/hpms/services/EmailNotificationService.java create mode 100644 tests/com/hpms/services/EmailNotificationServiceTest.java diff --git a/services/com/hpms/services/EmailNotificationService.java b/services/com/hpms/services/EmailNotificationService.java new file mode 100644 index 0000000..b8f637a --- /dev/null +++ b/services/com/hpms/services/EmailNotificationService.java @@ -0,0 +1,163 @@ +package com.hpms.services; + +import com.hpms.domain.Alert; +import com.hpms.services.exceptions.BusinessRuleException; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +/** + * EmailNotificationService — sends email notifications for critical alerts. + * + * Acceptance Criteria (FR-06 / Issue #7): + * - Critical alert email delivered within 60 seconds + * - Email includes patient name, vital type, triggered value, and timestamp + * - Delivery is logged in the system + * + * Design note: Uses a pluggable EmailSender interface so the real SMTP + * implementation can be swapped in without changing business logic. + * In tests, a mock/in-memory sender is injected instead. + */ +@Service +public class EmailNotificationService { + + private static final Logger LOGGER = Logger.getLogger(EmailNotificationService.class.getName()); + private static final String CRITICAL_SEVERITY = "CRITICAL"; + + private final EmailSender emailSender; + private final List deliveryLog = new ArrayList<>(); + + public EmailNotificationService(EmailSender emailSender) { + this.emailSender = emailSender; + } + + // ── Public API ──────────────────────────────────────────────────────── + + /** + * Sends a critical alert email to the given doctor email address. + * Logs delivery result regardless of outcome. + * + * @param alert the alert that was triggered + * @param patientName the full name of the patient + * @param doctorEmail the email address of the notified doctor + */ + public void notifyDoctor(Alert alert, String patientName, String doctorEmail) { + validateRequired(patientName, "patientName"); + validateRequired(doctorEmail, "doctorEmail"); + if (alert == null) { + throw new BusinessRuleException("alert must not be null."); + } + if (!CRITICAL_SEVERITY.equalsIgnoreCase(alert.getSeverity())) { + throw new BusinessRuleException( + "Email notifications are only sent for CRITICAL alerts. " + + "Received severity: " + alert.getSeverity() + ); + } + + String subject = buildSubject(patientName, alert); + String body = buildBody(patientName, alert); + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + + boolean success = false; + try { + emailSender.send(doctorEmail, subject, body); + success = true; + LOGGER.info(String.format( + "[NOTIFICATION] Critical alert email sent to %s for patient %s at %s", + doctorEmail, patientName, timestamp + )); + } catch (Exception e) { + LOGGER.severe(String.format( + "[NOTIFICATION] Failed to send critical alert email to %s: %s", + doctorEmail, e.getMessage() + )); + } + + deliveryLog.add(new NotificationLog( + alert.getAlertId().toString(), + patientName, + doctorEmail, + timestamp, + success + )); + } + + /** + * Returns an unmodifiable view of the delivery log. + */ + public List getDeliveryLog() { + return Collections.unmodifiableList(deliveryLog); + } + + // ── Email content builders ──────────────────────────────────────────── + + private String buildSubject(String patientName, Alert alert) { + return String.format( + "[CRITICAL ALERT] Patient: %s — Vital: %s", + patientName, alert.getVitalType() + ); + } + + private String buildBody(String patientName, Alert alert) { + String timestamp = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + return String.format( + "CRITICAL ALERT NOTIFICATION\n" + + "===========================\n" + + "Patient Name : %s\n" + + "Vital Type : %s\n" + + "Triggered Value: %.2f\n" + + "Severity : %s\n" + + "Timestamp : %s\n" + + "===========================\n" + + "Please log in to the Hospital Patient Monitoring System immediately.\n", + patientName, + alert.getVitalType(), + alert.getTriggeredValue(), + alert.getSeverity(), + timestamp + ); + } + + // ── Validation ──────────────────────────────────────────────────────── + + private void validateRequired(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new BusinessRuleException(fieldName + " is required."); + } + } + + // ── Inner types ─────────────────────────────────────────────────────── + + /** + * Pluggable email sender — implement with JavaMail/SMTP for production. + */ + public interface EmailSender { + void send(String to, String subject, String body); + } + + /** + * Immutable log entry for a single notification attempt. + */ + public static class NotificationLog { + public final String alertId; + public final String patientName; + public final String doctorEmail; + public final String timestamp; + public final boolean success; + + public NotificationLog(String alertId, String patientName, + String doctorEmail, String timestamp, boolean success) { + this.alertId = alertId; + this.patientName = patientName; + this.doctorEmail = doctorEmail; + this.timestamp = timestamp; + this.success = success; + } + } +} \ No newline at end of file diff --git a/tests/com/hpms/services/EmailNotificationServiceTest.java b/tests/com/hpms/services/EmailNotificationServiceTest.java new file mode 100644 index 0000000..794334a --- /dev/null +++ b/tests/com/hpms/services/EmailNotificationServiceTest.java @@ -0,0 +1,213 @@ +package com.hpms.services; + +import com.hpms.domain.Alert; +import com.hpms.repositories.inmemory.InMemoryAlertRepository; +import com.hpms.services.exceptions.BusinessRuleException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * EmailNotificationServiceTest — unit tests for critical alert email notifications. + * Relates to: Issue #7 — Email notifications for critical alerts (FR-06) + */ +class EmailNotificationServiceTest { + + // ── In-memory mock email sender ─────────────────────────────────────── + + static class MockEmailSender implements EmailNotificationService.EmailSender { + final List sentTo = new ArrayList<>(); + final List subjects = new ArrayList<>(); + final List bodies = new ArrayList<>(); + boolean shouldFail = false; + + @Override + public void send(String to, String subject, String body) { + if (shouldFail) throw new RuntimeException("SMTP unavailable"); + sentTo.add(to); + subjects.add(subject); + bodies.add(body); + } + } + + private AlertService alertService; + private MockEmailSender mockSender; + private EmailNotificationService notificationService; + + @BeforeEach + void setUp() { + alertService = new AlertService(new InMemoryAlertRepository()); + mockSender = new MockEmailSender(); + notificationService = new EmailNotificationService(mockSender); + } + + // ── 1. Happy path ───────────────────────────────────────────────────── + + @Test + void notifyDoctor_sendsCriticalAlertEmail() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + ); + + notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com"); + + assertEquals(1, mockSender.sentTo.size()); + assertEquals("doctor@hospital.com", mockSender.sentTo.get(0)); + } + + @Test + void notifyDoctor_emailSubjectContainsPatientNameAndVitalType() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + ); + + notificationService.notifyDoctor(alert, "Jane Smith", "doctor@hospital.com"); + + String subject = mockSender.subjects.get(0); + assertTrue(subject.contains("Jane Smith")); + assertTrue(subject.contains("heartRate")); + } + + @Test + void notifyDoctor_emailBodyContainsAllRequiredFields() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 190.0, "CRITICAL" + ); + + notificationService.notifyDoctor(alert, "Alice Brown", "doctor@hospital.com"); + + String body = mockSender.bodies.get(0); + assertTrue(body.contains("Alice Brown"), "body should contain patient name"); + assertTrue(body.contains("bloodPressure"), "body should contain vital type"); + assertTrue(body.contains("190.00"), "body should contain triggered value"); + assertTrue(body.contains("CRITICAL"), "body should contain severity"); + } + + @Test + void notifyDoctor_emailBodyContainsTimestamp() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" + ); + + notificationService.notifyDoctor(alert, "Bob Green", "doctor@hospital.com"); + + String body = mockSender.bodies.get(0); + assertTrue(body.contains("Timestamp"), "body should contain a timestamp field"); + } + + // ── 2. Delivery log ─────────────────────────────────────────────────── + + @Test + void notifyDoctor_logsSuccessfulDelivery() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 175.0, "CRITICAL" + ); + + notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com"); + + List log = notificationService.getDeliveryLog(); + assertEquals(1, log.size()); + assertTrue(log.get(0).success); + assertEquals("John Doe", log.get(0).patientName); + assertEquals("doctor@hospital.com", log.get(0).doctorEmail); + } + + @Test + void notifyDoctor_logsFailedDelivery_whenSenderThrows() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 175.0, "CRITICAL" + ); + mockSender.shouldFail = true; + + notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com"); + + List log = notificationService.getDeliveryLog(); + assertEquals(1, log.size()); + assertFalse(log.get(0).success); + } + + @Test + void notifyDoctor_multipleAlerts_allLogged() { + Alert alert1 = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + ); + Alert alert2 = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 82.0, "CRITICAL" + ); + + notificationService.notifyDoctor(alert1, "Patient A", "doctor1@hospital.com"); + notificationService.notifyDoctor(alert2, "Patient B", "doctor2@hospital.com"); + + assertEquals(2, notificationService.getDeliveryLog().size()); + assertEquals(2, mockSender.sentTo.size()); + } + + // ── 3. Non-critical alerts blocked ─────────────────────────────────── + + @Test + void notifyDoctor_throwsForWarningAlert() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, "WARNING" + ); + + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com") + ); + assertTrue(ex.getMessage().contains("CRITICAL")); + assertEquals(0, mockSender.sentTo.size()); + } + + // ── 4. Input validation ─────────────────────────────────────────────── + + @Test + void notifyDoctor_throwsWhenAlertIsNull() { + assertThrows(BusinessRuleException.class, () -> + notificationService.notifyDoctor(null, "John Doe", "doctor@hospital.com") + ); + } + + @Test + void notifyDoctor_throwsWhenPatientNameIsNull() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + ); + assertThrows(BusinessRuleException.class, () -> + notificationService.notifyDoctor(alert, null, "doctor@hospital.com") + ); + } + + @Test + void notifyDoctor_throwsWhenPatientNameIsBlank() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + ); + assertThrows(BusinessRuleException.class, () -> + notificationService.notifyDoctor(alert, " ", "doctor@hospital.com") + ); + } + + @Test + void notifyDoctor_throwsWhenDoctorEmailIsNull() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + ); + assertThrows(BusinessRuleException.class, () -> + notificationService.notifyDoctor(alert, "John Doe", null) + ); + } + + @Test + void notifyDoctor_throwsWhenDoctorEmailIsBlank() { + Alert alert = alertService.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + ); + assertThrows(BusinessRuleException.class, () -> + notificationService.notifyDoctor(alert, "John Doe", "") + ); + } +} \ No newline at end of file From 66a4b80c3d309897298edb4f1527baf375d1c5b4 Mon Sep 17 00:00:00 2001 From: Mpumlwana Lakhikhaya <220204594@mycput.ac.za> Date: Mon, 8 Jun 2026 04:16:24 +0200 Subject: [PATCH 5/6] updated --- services/{com/hpms/services => }/EmailNotificationService.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename services/{com/hpms/services => }/EmailNotificationService.java (100%) diff --git a/services/com/hpms/services/EmailNotificationService.java b/services/EmailNotificationService.java similarity index 100% rename from services/com/hpms/services/EmailNotificationService.java rename to services/EmailNotificationService.java From 54884c72d6eb20f0de812ee607503292f78e58dc Mon Sep 17 00:00:00 2001 From: Mpumlwana Lakhikhaya <220204594@mycput.ac.za> Date: Mon, 8 Jun 2026 04:32:10 +0200 Subject: [PATCH 6/6] Commit --- services/EmailNotificationService.java | 52 +--- .../EmailNotificationServiceTest.java | 279 ++++++++++-------- 2 files changed, 166 insertions(+), 165 deletions(-) diff --git a/services/EmailNotificationService.java b/services/EmailNotificationService.java index b8f637a..e9478c8 100644 --- a/services/EmailNotificationService.java +++ b/services/EmailNotificationService.java @@ -2,7 +2,6 @@ import com.hpms.domain.Alert; import com.hpms.services.exceptions.BusinessRuleException; -import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -13,17 +12,11 @@ /** * EmailNotificationService — sends email notifications for critical alerts. + * Issue #7 — Email notifications for critical alerts (FR-06) * - * Acceptance Criteria (FR-06 / Issue #7): - * - Critical alert email delivered within 60 seconds - * - Email includes patient name, vital type, triggered value, and timestamp - * - Delivery is logged in the system - * - * Design note: Uses a pluggable EmailSender interface so the real SMTP - * implementation can be swapped in without changing business logic. - * In tests, a mock/in-memory sender is injected instead. + * NOTE: Not annotated with @Service to avoid Spring requiring an EmailSender bean. + * Instantiate manually or wire with a concrete EmailSender in your config. */ -@Service public class EmailNotificationService { private static final Logger LOGGER = Logger.getLogger(EmailNotificationService.class.getName()); @@ -36,16 +29,6 @@ public EmailNotificationService(EmailSender emailSender) { this.emailSender = emailSender; } - // ── Public API ──────────────────────────────────────────────────────── - - /** - * Sends a critical alert email to the given doctor email address. - * Logs delivery result regardless of outcome. - * - * @param alert the alert that was triggered - * @param patientName the full name of the patient - * @param doctorEmail the email address of the notified doctor - */ public void notifyDoctor(Alert alert, String patientName, String doctorEmail) { validateRequired(patientName, "patientName"); validateRequired(doctorEmail, "doctorEmail"); @@ -59,8 +42,8 @@ public void notifyDoctor(Alert alert, String patientName, String doctorEmail) { ); } - String subject = buildSubject(patientName, alert); - String body = buildBody(patientName, alert); + String subject = buildSubject(patientName, alert); + String body = buildBody(patientName, alert); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); boolean success = false; @@ -87,15 +70,10 @@ public void notifyDoctor(Alert alert, String patientName, String doctorEmail) { )); } - /** - * Returns an unmodifiable view of the delivery log. - */ public List getDeliveryLog() { return Collections.unmodifiableList(deliveryLog); } - // ── Email content builders ──────────────────────────────────────────── - private String buildSubject(String patientName, Alert alert) { return String.format( "[CRITICAL ALERT] Patient: %s — Vital: %s", @@ -109,11 +87,11 @@ private String buildBody(String patientName, Alert alert) { return String.format( "CRITICAL ALERT NOTIFICATION\n" + "===========================\n" + - "Patient Name : %s\n" + - "Vital Type : %s\n" + + "Patient Name : %s\n" + + "Vital Type : %s\n" + "Triggered Value: %.2f\n" + - "Severity : %s\n" + - "Timestamp : %s\n" + + "Severity : %s\n" + + "Timestamp : %s\n" + "===========================\n" + "Please log in to the Hospital Patient Monitoring System immediately.\n", patientName, @@ -124,26 +102,20 @@ private String buildBody(String patientName, Alert alert) { ); } - // ── Validation ──────────────────────────────────────────────────────── - private void validateRequired(String value, String fieldName) { if (value == null || value.isBlank()) { throw new BusinessRuleException(fieldName + " is required."); } } - // ── Inner types ─────────────────────────────────────────────────────── + // ── Pluggable sender interface ───────────────────────────────────────── - /** - * Pluggable email sender — implement with JavaMail/SMTP for production. - */ public interface EmailSender { void send(String to, String subject, String body); } - /** - * Immutable log entry for a single notification attempt. - */ + // ── Delivery log entry ──────────────────────────────────────────────── + public static class NotificationLog { public final String alertId; public final String patientName; diff --git a/tests/com/hpms/services/EmailNotificationServiceTest.java b/tests/com/hpms/services/EmailNotificationServiceTest.java index 794334a..7ff706a 100644 --- a/tests/com/hpms/services/EmailNotificationServiceTest.java +++ b/tests/com/hpms/services/EmailNotificationServiceTest.java @@ -3,211 +3,240 @@ import com.hpms.domain.Alert; import com.hpms.repositories.inmemory.InMemoryAlertRepository; import com.hpms.services.exceptions.BusinessRuleException; +import com.hpms.services.exceptions.ResourceNotFoundException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.ArrayList; import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; /** - * EmailNotificationServiceTest — unit tests for critical alert email notifications. - * Relates to: Issue #7 — Email notifications for critical alerts (FR-06) + * AlertServiceThresholdTest — Integration Tests for AlertService + * Relates to: Issue #33 — Write integration test for AlertService threshold */ -class EmailNotificationServiceTest { +class AlertServiceThresholdTest { - // ── In-memory mock email sender ─────────────────────────────────────── - - static class MockEmailSender implements EmailNotificationService.EmailSender { - final List sentTo = new ArrayList<>(); - final List subjects = new ArrayList<>(); - final List bodies = new ArrayList<>(); - boolean shouldFail = false; - - @Override - public void send(String to, String subject, String body) { - if (shouldFail) throw new RuntimeException("SMTP unavailable"); - sentTo.add(to); - subjects.add(subject); - bodies.add(body); - } - } - - private AlertService alertService; - private MockEmailSender mockSender; - private EmailNotificationService notificationService; + private AlertService service; @BeforeEach void setUp() { - alertService = new AlertService(new InMemoryAlertRepository()); - mockSender = new MockEmailSender(); - notificationService = new EmailNotificationService(mockSender); + service = new AlertService(new InMemoryAlertRepository()); } - // ── 1. Happy path ───────────────────────────────────────────────────── + // ── 1. Threshold / triggeredValue validation ────────────────────────── @Test - void notifyDoctor_sendsCriticalAlertEmail() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + void createAlert_throwsWhenTriggeredValueIsZero() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 0, "WARNING") ); - - notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com"); - - assertEquals(1, mockSender.sentTo.size()); - assertEquals("doctor@hospital.com", mockSender.sentTo.get(0)); + assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); } @Test - void notifyDoctor_emailSubjectContainsPatientNameAndVitalType() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + void createAlert_throwsWhenTriggeredValueIsNegative() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", -10.0, "CRITICAL") ); + assertTrue(ex.getMessage().contains("triggeredValue must be greater than 0")); + } - notificationService.notifyDoctor(alert, "Jane Smith", "doctor@hospital.com"); + @Test + void createAlert_succeedsWithValidThreshold() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" + ); + assertNotNull(alert); + assertEquals(140.0, alert.getTriggeredValue()); + assertEquals("WARNING", alert.getSeverity()); + } - String subject = mockSender.subjects.get(0); - assertTrue(subject.contains("Jane Smith")); - assertTrue(subject.contains("heartRate")); + @Test + void createAlert_succeedsWithMinimalPositiveThreshold() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 0.1, "WARNING" + ); + assertEquals(0.1, alert.getTriggeredValue()); } @Test - void notifyDoctor_emailBodyContainsAllRequiredFields() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 190.0, "CRITICAL" + void createAlert_succeedsWithHighThresholdValue() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 999.9, "CRITICAL" ); + assertEquals(999.9, alert.getTriggeredValue()); + } - notificationService.notifyDoctor(alert, "Alice Brown", "doctor@hospital.com"); + // ── 2. Required field validation ────────────────────────────────────── - String body = mockSender.bodies.get(0); - assertTrue(body.contains("Alice Brown"), "body should contain patient name"); - assertTrue(body.contains("bloodPressure"), "body should contain vital type"); - assertTrue(body.contains("190.00"), "body should contain triggered value"); - assertTrue(body.contains("CRITICAL"), "body should contain severity"); + @Test + void createAlert_throwsWhenVitalTypeIsNull() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), null, 120.0, "WARNING") + ); + assertTrue(ex.getMessage().contains("vitalType is required")); } @Test - void notifyDoctor_emailBodyContainsTimestamp() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" + void createAlert_throwsWhenVitalTypeIsBlank() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), " ", 120.0, "WARNING") ); - - notificationService.notifyDoctor(alert, "Bob Green", "doctor@hospital.com"); - - String body = mockSender.bodies.get(0); - assertTrue(body.contains("Timestamp"), "body should contain a timestamp field"); + assertTrue(ex.getMessage().contains("vitalType is required")); } - // ── 2. Delivery log ─────────────────────────────────────────────────── + @Test + void createAlert_throwsWhenSeverityIsNull() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, null) + ); + assertTrue(ex.getMessage().contains("severity is required")); + } @Test - void notifyDoctor_logsSuccessfulDelivery() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 175.0, "CRITICAL" + void createAlert_throwsWhenSeverityIsBlank() { + BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, "") ); + assertTrue(ex.getMessage().contains("severity is required")); + } - notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com"); + // ── 3. State transitions ────────────────────────────────────────────── - List log = notificationService.getDeliveryLog(); - assertEquals(1, log.size()); - assertTrue(log.get(0).success); - assertEquals("John Doe", log.get(0).patientName); - assertEquals("doctor@hospital.com", log.get(0).doctorEmail); + @Test + void triggerAlert_changesStatusAwayFromOpen() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 150.0, "CRITICAL" + ); + Alert triggered = service.triggerAlert(alert.getAlertId()); + // Status should no longer be OPEN after triggering + assertNotEquals("OPEN", triggered.getStatus()); } @Test - void notifyDoctor_logsFailedDelivery_whenSenderThrows() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 175.0, "CRITICAL" + void acknowledgeAlert_afterTrigger_changesStatusToAcknowledged() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" ); - mockSender.shouldFail = true; - - notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com"); - - List log = notificationService.getDeliveryLog(); - assertEquals(1, log.size()); - assertFalse(log.get(0).success); + service.triggerAlert(alert.getAlertId()); + Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); } @Test - void notifyDoctor_multipleAlerts_allLogged() { - Alert alert1 = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + void closeAlert_changesStatusToClosed() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" ); - Alert alert2 = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 82.0, "CRITICAL" + service.triggerAlert(alert.getAlertId()); + service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + Alert closed = service.closeAlert(alert.getAlertId()); + assertEquals("CLOSED", closed.getStatus()); + } + + @Test + void fullAlertLifecycle_createdToClosedSuccessfully() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "temperature", 39.5, "WARNING" ); + assertNotNull(alert.getAlertId()); - notificationService.notifyDoctor(alert1, "Patient A", "doctor1@hospital.com"); - notificationService.notifyDoctor(alert2, "Patient B", "doctor2@hospital.com"); + // Trigger — status changes away from OPEN + Alert triggered = service.triggerAlert(alert.getAlertId()); + assertNotEquals("OPEN", triggered.getStatus()); - assertEquals(2, notificationService.getDeliveryLog().size()); - assertEquals(2, mockSender.sentTo.size()); + // Acknowledge + Alert acknowledged = service.acknowledgeAlert(alert.getAlertId(), UUID.randomUUID()); + assertEquals("ACKNOWLEDGED", acknowledged.getStatus()); + + // Close + Alert closed = service.closeAlert(alert.getAlertId()); + assertEquals("CLOSED", closed.getStatus()); } - // ── 3. Non-critical alerts blocked ─────────────────────────────────── + // ── 4. Retrieval ────────────────────────────────────────────────────── @Test - void notifyDoctor_throwsForWarningAlert() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 120.0, "WARNING" + void getAlertById_returnsCorrectAlert() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 130.0, "WARNING" ); + Alert fetched = service.getAlertById(alert.getAlertId()); + assertEquals(alert.getAlertId(), fetched.getAlertId()); + } - BusinessRuleException ex = assertThrows(BusinessRuleException.class, () -> - notificationService.notifyDoctor(alert, "John Doe", "doctor@hospital.com") + @Test + void getAlertById_throwsWhenNotFound() { + assertThrows(ResourceNotFoundException.class, () -> + service.getAlertById(UUID.randomUUID()) ); - assertTrue(ex.getMessage().contains("CRITICAL")); - assertEquals(0, mockSender.sentTo.size()); } - // ── 4. Input validation ─────────────────────────────────────────────── + @Test + void getAllAlerts_returnsAllCreatedAlerts() { + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING"); + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "bloodPressure", 180.0, "CRITICAL"); + service.createAlert(UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 88.0, "WARNING"); + + List alerts = service.getAllAlerts(); + assertEquals(3, alerts.size()); + } @Test - void notifyDoctor_throwsWhenAlertIsNull() { - assertThrows(BusinessRuleException.class, () -> - notificationService.notifyDoctor(null, "John Doe", "doctor@hospital.com") - ); + void getAllAlerts_returnsEmptyListWhenNoAlerts() { + List alerts = service.getAllAlerts(); + assertTrue(alerts.isEmpty()); } + // ── 5. Delete ───────────────────────────────────────────────────────── + @Test - void notifyDoctor_throwsWhenPatientNameIsNull() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + void deleteAlert_removesAlertFromRepository() { + Alert alert = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" ); - assertThrows(BusinessRuleException.class, () -> - notificationService.notifyDoctor(alert, null, "doctor@hospital.com") + service.deleteAlert(alert.getAlertId()); + assertThrows(ResourceNotFoundException.class, () -> + service.getAlertById(alert.getAlertId()) ); } @Test - void notifyDoctor_throwsWhenPatientNameIsBlank() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" - ); - assertThrows(BusinessRuleException.class, () -> - notificationService.notifyDoctor(alert, " ", "doctor@hospital.com") + void deleteAlert_throwsWhenAlertDoesNotExist() { + assertThrows(ResourceNotFoundException.class, () -> + service.deleteAlert(UUID.randomUUID()) ); } + // ── 6. Multiple patients / isolation ───────────────────────────────── + @Test - void notifyDoctor_throwsWhenDoctorEmailIsNull() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" - ); - assertThrows(BusinessRuleException.class, () -> - notificationService.notifyDoctor(alert, "John Doe", null) - ); + void multipleAlerts_forDifferentPatients_areStoredIndependently() { + UUID patient1 = UUID.randomUUID(); + UUID patient2 = UUID.randomUUID(); + + Alert alert1 = service.createAlert(patient1, UUID.randomUUID(), "heartRate", 140.0, "WARNING"); + Alert alert2 = service.createAlert(patient2, UUID.randomUUID(), "bloodPressure", 190.0, "CRITICAL"); + + assertNotEquals(alert1.getAlertId(), alert2.getAlertId()); + assertEquals(2, service.getAllAlerts().size()); } @Test - void notifyDoctor_throwsWhenDoctorEmailIsBlank() { - Alert alert = alertService.createAlert( - UUID.randomUUID(), UUID.randomUUID(), "heartRate", 180.0, "CRITICAL" + void triggeringOneAlert_doesNotAffectOtherAlerts() { + Alert alert1 = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "heartRate", 140.0, "WARNING" ); - assertThrows(BusinessRuleException.class, () -> - notificationService.notifyDoctor(alert, "John Doe", "") + Alert alert2 = service.createAlert( + UUID.randomUUID(), UUID.randomUUID(), "oxygenLevel", 85.0, "CRITICAL" ); + + service.triggerAlert(alert1.getAlertId()); + + Alert fetched2 = service.getAlertById(alert2.getAlertId()); + assertNotEquals("ACKNOWLEDGED", fetched2.getStatus()); + assertNotEquals("CLOSED", fetched2.getStatus()); } } \ No newline at end of file