diff --git a/server/messaging/reporting.py b/server/messaging/reporting.py index 72eafb04d..a1c96fb68 100755 --- a/server/messaging/reporting.py +++ b/server/messaging/reporting.py @@ -99,6 +99,19 @@ def apply_timezone(data, fields): # =============================================================================== # REPORTING # =============================================================================== +MAX_ALERT_DOWN_MINUTES = 100 * 365 * 24 * 60 + + +def get_alert_down_minutes(): + """Return NTFPRCS_alert_down_time as whole minutes clamped to + 0..MAX_ALERT_DOWN_MINUTES; a non-numeric value counts as 0.""" + try: + minutes = int(get_setting_value("NTFPRCS_alert_down_time") or 0) + except (TypeError, ValueError): + return 0 + return max(0, min(minutes, MAX_ALERT_DOWN_MINUTES)) + + def get_notifications(db): """ Fetch notifications for all configured sections. @@ -129,7 +142,7 @@ def get_notifications(db): # Computed once and shared with NotificationInstance.clearPendingEmailFlag() # via conf, so the clear step acts on exactly the events selected here. - alert_down_minutes = int(get_setting_value("NTFPRCS_alert_down_time") or 0) + alert_down_minutes = get_alert_down_minutes() alert_down_cutoff = ( timeNowUTC(as_string=False) - timedelta(minutes=alert_down_minutes) ).strftime(DATETIME_PATTERN) diff --git a/server/models/notification_instance.py b/server/models/notification_instance.py index 32a7fb2ef..d07229d2e 100755 --- a/server/models/notification_instance.py +++ b/server/models/notification_instance.py @@ -235,7 +235,8 @@ class NotificationInstance: def clearPendingEmailFlag(self): """Clear evePendingAlertEmail on reported Events and stamp devLastNotification on their devices; 'Device Down' events are only - cleared once NTFPRCS_alert_down_time minutes have passed.""" + cleared if older than conf.down_alert_cutoff, the cutoff + get_notifications() used when selecting them.""" # Clean Pending Alert Events self.db.sql.execute(""" diff --git a/test/backend/test_notification_down_clear_timezone.py b/test/backend/test_notification_down_clear_timezone.py index bb2f8e929..1e629d6a4 100644 --- a/test/backend/test_notification_down_clear_timezone.py +++ b/test/backend/test_notification_down_clear_timezone.py @@ -275,5 +275,45 @@ class TestClearPendingEmailFlagSelectionCutoff(unittest.TestCase): self.assertLessEqual(cutoff_dt, after - timedelta(minutes=5)) +class TestAlertDownTimeBounds(unittest.TestCase): + """An out-of-range NTFPRCS_alert_down_time must never raise out of + get_notifications() (it runs inside the main monitoring loop).""" + + def _run_get_notifications(self, raw_minutes): + """Run get_notifications() with the given raw setting value and return + the recorded cutoff as a UTC datetime, restoring conf afterward.""" + mock_db = Mock() + mock_json_obj = Mock() + mock_json_obj.columnNames = [] + mock_json_obj.json = {"data": []} + mock_db.get_table_as_json.return_value = mock_json_obj + settings = { + "NTFPRCS_INCLUDED_SECTIONS": ["down_devices"], + "NTFPRCS_alert_down_time": raw_minutes, + } + original_cutoff = conf.down_alert_cutoff + try: + with patch( + "messaging.reporting.get_setting_value", + side_effect=lambda key: settings.get(key, ""), + ): + get_notifications(mock_db) + cutoff = conf.down_alert_cutoff + finally: + conf.down_alert_cutoff = original_cutoff + return datetime.strptime(cutoff, DATETIME_PATTERN).replace(tzinfo=timezone.utc) + + def test_huge_value_does_not_raise(self): + """A value too large for timedelta/datetime must be bounded, not raise + OverflowError.""" + cutoff = self._run_get_notifications("999999999999999") + self.assertLess(cutoff, timeNowUTC(as_string=False)) + + def test_negative_value_does_not_produce_future_cutoff(self): + """A negative window must not push the cutoff into the future.""" + cutoff = self._run_get_notifications("-5") + self.assertLessEqual(cutoff, timeNowUTC(as_string=False)) + + if __name__ == "__main__": unittest.main()