diff --git a/plugins/endpoints/api/routes.py b/plugins/endpoints/api/routes.py index d7dd772e..e4ded727 100644 --- a/plugins/endpoints/api/routes.py +++ b/plugins/endpoints/api/routes.py @@ -4148,6 +4148,9 @@ def get_fleet_alerts(): NotificationAcknowledgement.assetid == asset_id)) .filter(Notification.alertfleet.is_(True), Notification.isactive.is_(True), + db.or_( + Notification.starttime.is_(None), + Notification.starttime <= now), db.or_( Notification.endtime.is_(None), Notification.endtime > now), diff --git a/tests/test_plugins/test_endpoints_fleet_alert_scheduling.py b/tests/test_plugins/test_endpoints_fleet_alert_scheduling.py new file mode 100644 index 00000000..b034332a --- /dev/null +++ b/tests/test_plugins/test_endpoints_fleet_alert_scheduling.py @@ -0,0 +1,254 @@ +"""Test fleet alert scheduling respects starttime. + +Regression test for bug where fleet alerts with future starttime were sent +immediately instead of waiting for the scheduled time. +""" + +import pytest +from datetime import datetime, timedelta, timezone + + +def test_fleet_alert_respects_future_starttime(client, auth_headers, sample_asset): + """Fleet alerts with future starttime should not appear until that time.""" + from plugins.notifications.models import Notification, NotificationType + from plugins.endpoints.models import EndpointState + from shopdb.api import db + + # Create notification type + ntype = NotificationType( + typename='System Maintenance', + typecolor='#ffc107', + isactive=True + ) + db.session.add(ntype) + db.session.flush() + + # Create endpoint state for the asset + state = EndpointState( + assetid=sample_asset.assetid, + hostname='TEST-PC-01', + agentversion='1.0.0', + lastreportedat=datetime.utcnow() + ) + db.session.add(state) + db.session.flush() + + now = datetime.now(timezone.utc).replace(tzinfo=None) + future_time = now + timedelta(minutes=20) # 20 minutes from now + end_time = future_time + timedelta(hours=1) + + # Create fleet alert scheduled for 20 minutes in the future + notification = Notification( + notification='Scheduled maintenance at 3:30 PM', + notificationtypeid=ntype.notificationtypeid, + starttime=future_time, + endtime=end_time, + isactive=True, + alertfleet=True, + alertscope=None, # All endpoints + alerttimeout=0 + ) + db.session.add(notification) + db.session.commit() + + # Agent polls for fleet alerts NOW (before starttime) + response = client.get( + '/api/endpoints/alerts?hostname=TEST-PC-01', + headers=auth_headers + ) + + assert response.status_code == 200 + data = response.get_json() + assert data['status'] == 'success' + + # BUG FIX: Alert should NOT appear because starttime is in the future + alerts = data['data'] + assert len(alerts) == 0, 'Fleet alert with future starttime should not appear yet' + + # Simulate time passing - set starttime to the past + notification.starttime = now - timedelta(minutes=5) + db.session.commit() + + # Agent polls again (now starttime has passed) + response = client.get( + '/api/endpoints/alerts?hostname=TEST-PC-01', + headers=auth_headers + ) + + assert response.status_code == 200 + data = response.get_json() + assert data['status'] == 'success' + + # Now the alert SHOULD appear + alerts = data['data'] + assert len(alerts) == 1 + assert alerts[0]['notificationId'] == notification.notificationid + assert alerts[0]['title'] == 'Scheduled maintenance at 3:30 PM' + + +def test_fleet_alert_with_no_starttime_appears_immediately(client, auth_headers, sample_asset): + """Fleet alerts with no starttime should appear immediately.""" + from plugins.notifications.models import Notification, NotificationType + from plugins.endpoints.models import EndpointState + from shopdb.api import db + + # Create notification type + ntype = NotificationType( + typename='System Alert', + typecolor='#dc3545', + isactive=True + ) + db.session.add(ntype) + db.session.flush() + + # Create endpoint state + state = EndpointState( + assetid=sample_asset.assetid, + hostname='TEST-PC-02', + agentversion='1.0.0', + lastreportedat=datetime.utcnow() + ) + db.session.add(state) + db.session.flush() + + # Create fleet alert with no starttime (immediate) + notification = Notification( + notification='Immediate alert - urgent', + notificationtypeid=ntype.notificationtypeid, + starttime=None, # No starttime = immediate + endtime=None, + isactive=True, + alertfleet=True, + alertscope=None, + alerttimeout=0 + ) + db.session.add(notification) + db.session.commit() + + # Agent polls for fleet alerts + response = client.get( + '/api/endpoints/alerts?hostname=TEST-PC-02', + headers=auth_headers + ) + + assert response.status_code == 200 + data = response.get_json() + assert data['status'] == 'success' + + # Alert should appear immediately + alerts = data['data'] + assert len(alerts) == 1 + assert alerts[0]['notificationId'] == notification.notificationid + assert alerts[0]['title'] == 'Immediate alert - urgent' + + +def test_fleet_alert_past_starttime_appears(client, auth_headers, sample_asset): + """Fleet alerts with past starttime should appear.""" + from plugins.notifications.models import Notification, NotificationType + from plugins.endpoints.models import EndpointState + from shopdb.api import db + + ntype = NotificationType( + typename='Update', + typecolor='#17a2b8', + isactive=True + ) + db.session.add(ntype) + db.session.flush() + + state = EndpointState( + assetid=sample_asset.assetid, + hostname='TEST-PC-03', + agentversion='1.0.0', + lastreportedat=datetime.utcnow() + ) + db.session.add(state) + db.session.flush() + + now = datetime.now(timezone.utc).replace(tzinfo=None) + past_time = now - timedelta(hours=1) + + # Create fleet alert that started 1 hour ago + notification = Notification( + notification='Alert started 1 hour ago', + notificationtypeid=ntype.notificationtypeid, + starttime=past_time, + endtime=None, + isactive=True, + alertfleet=True, + alertscope=None, + alerttimeout=0 + ) + db.session.add(notification) + db.session.commit() + + # Agent polls for fleet alerts + response = client.get( + '/api/endpoints/alerts?hostname=TEST-PC-03', + headers=auth_headers + ) + + assert response.status_code == 200 + data = response.get_json() + assert data['status'] == 'success' + + # Alert should appear (starttime is in the past) + alerts = data['data'] + assert len(alerts) == 1 + assert alerts[0]['notificationId'] == notification.notificationid + + +def test_fleet_alert_expired_does_not_appear(client, auth_headers, sample_asset): + """Fleet alerts with past endtime should not appear.""" + from plugins.notifications.models import Notification, NotificationType + from plugins.endpoints.models import EndpointState + from shopdb.api import db + + ntype = NotificationType( + typename='Expired', + typecolor='#6c757d', + isactive=True + ) + db.session.add(ntype) + db.session.flush() + + state = EndpointState( + assetid=sample_asset.assetid, + hostname='TEST-PC-04', + agentversion='1.0.0', + lastreportedat=datetime.utcnow() + ) + db.session.add(state) + db.session.flush() + + now = datetime.now(timezone.utc).replace(tzinfo=None) + past_start = now - timedelta(hours=2) + past_end = now - timedelta(hours=1) + + # Create expired fleet alert + notification = Notification( + notification='This alert already ended', + notificationtypeid=ntype.notificationtypeid, + starttime=past_start, + endtime=past_end, # Ended 1 hour ago + isactive=True, + alertfleet=True, + alertscope=None, + alerttimeout=0 + ) + db.session.add(notification) + db.session.commit() + + # Agent polls for fleet alerts + response = client.get( + '/api/endpoints/alerts?hostname=TEST-PC-04', + headers=auth_headers + ) + + assert response.status_code == 200 + data = response.get_json() + assert data['status'] == 'success' + + # Alert should NOT appear (endtime is in the past) + alerts = data['data'] + assert len(alerts) == 0