Optimizing polling (#2946)

* Optimizing polling

* Fixed formatting issue
This commit is contained in:
Alex Janousek
2025-09-26 06:57:18 -04:00
committed by GitHub
parent 5a9c9824aa
commit a40c8861bf
15 changed files with 528 additions and 163 deletions

View File

@@ -0,0 +1,141 @@
"""
Tests for job notification update logic during polling.
These tests verify the poll status endpoint behavior and document
the JavaScript notification refresh logic:
1. Notifications update for first 50 messages
2. Notifications stop updating after 50 messages (to prevent performance issues)
3. Notifications always update when job finishes
"""
import json
import pytest
from tests import job_json, user_json
@pytest.mark.parametrize(
("delivered", "failed", "pending", "finished", "js_should_update_notifications", "reason"),
[
(20, 10, 70, False, True, "30 messages processed (≤50 threshold)"),
(40, 10, 50, False, True, "50 messages processed (exactly at threshold)"),
(45, 15, 40, False, False, "60 messages processed (>50 threshold)"),
(450, 50, 0, True, True, "500 messages but job finished (always updates)"),
],
)
def test_poll_status_notification_update_logic(
client_request,
service_one,
active_user_with_permissions,
mock_get_service_data_retention,
mocker,
fake_uuid,
delivered,
failed,
pending,
finished,
js_should_update_notifications,
reason,
):
"""
Test poll status endpoint for various scenarios.
The JavaScript updates notifications when:
processedCount ≤ 50 AND job not finished
job is finished (regardless of count)
"""
total = delivered + failed + pending
job_status = "finished" if finished else "sending"
mock_job = mocker.patch("app.job_api_client.get_job")
mock_job.return_value = {
"data": {
**job_json(
service_one["id"],
created_by=user_json(),
job_id=fake_uuid,
job_status=job_status,
notification_count=total,
notifications_requested=total,
),
"statistics": [
{"status": "delivered", "count": delivered},
{"status": "failed", "count": failed},
{"status": "pending", "count": pending},
],
}
}
response = client_request.get_response(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
)
assert response.status_code == 200
data = json.loads(response.get_data(as_text=True))
# Verify the response
assert data["sent_count"] == delivered
assert data["failed_count"] == failed
assert data["pending_count"] == pending
assert data["total_count"] == total
assert data["finished"] is finished
processed_count = delivered + failed
if js_should_update_notifications:
# JavaScript would call: await updateNotifications()
if finished:
assert finished, f"JS updates notifications: {reason}"
else:
assert processed_count <= 50, f"JS updates notifications: {reason}"
assert not finished, f"JS updates notifications: {reason}"
else:
# JavaScript would NOT update notifications
assert processed_count > 50, f"JS skips notification update: {reason}"
assert not finished, f"JS skips notification update: {reason}"
def test_poll_status_provides_required_fields(
client_request,
service_one,
active_user_with_permissions,
mock_get_service_data_retention,
mocker,
fake_uuid,
):
"""Verify poll status endpoint returns all fields needed for notification update logic."""
mock_job = mocker.patch("app.job_api_client.get_job")
mock_job.return_value = {
"data": {
**job_json(
service_one["id"],
created_by=user_json(),
job_id=fake_uuid,
job_status="sending",
notification_count=25,
notifications_requested=25,
),
"statistics": [
{"status": "delivered", "count": 15},
{"status": "failed", "count": 5},
{"status": "pending", "count": 5},
],
}
}
response = client_request.get_response(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
)
data = json.loads(response.get_data(as_text=True))
required_fields = {"sent_count", "failed_count", "finished", "pending_count", "total_count"}
assert set(data.keys()) == required_fields
response_size = len(response.get_data(as_text=True))
assert response_size < 200, f"Response too large: {response_size} bytes"

View File

@@ -499,3 +499,143 @@ def test_should_show_message_note(
'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear '
"back from the carrier they have received it and attempted deliver. More information on delivery status."
)
def test_poll_status_endpoint(
client_request,
service_one,
active_user_with_permissions,
mock_get_service_data_retention,
mocker,
fake_uuid,
):
"""Test that the poll status endpoint returns only required data without notifications"""
mock_job = mocker.patch("app.job_api_client.get_job")
mock_job.return_value = {
"data": {
**job_json(
service_one["id"],
created_by=user_json(),
job_id=fake_uuid,
job_status="finished",
notification_count=100,
notifications_requested=100,
),
"statistics": [
{"status": "delivered", "count": 90},
{"status": "failed", "count": 10},
{"status": "pending", "count": 0},
],
}
}
response = client_request.get_response(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
)
assert response.status_code == 200
data = json.loads(response.get_data(as_text=True))
expected_keys = {
"sent_count",
"failed_count",
"pending_count",
"total_count",
"finished",
}
assert set(data.keys()) == expected_keys
assert data["sent_count"] == 90
assert data["failed_count"] == 10
assert data["pending_count"] == 0
assert data["total_count"] == 100
assert data["finished"] is True
def test_poll_status_with_zero_notifications(
client_request,
service_one,
active_user_with_permissions,
mock_get_service_data_retention,
mocker,
fake_uuid,
):
"""Test poll status endpoint handles edge case of no notifications"""
mock_job = mocker.patch("app.job_api_client.get_job")
mock_job.return_value = {
"data": {
**job_json(
service_one["id"],
created_by=user_json(),
job_id=fake_uuid,
job_status="pending",
notification_count=0,
notifications_requested=0,
),
"statistics": [],
}
}
response = client_request.get_response(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
)
assert response.status_code == 200
data = json.loads(response.get_data(as_text=True))
assert data["total_count"] == 0
assert (
data["finished"] is True
)
def test_poll_status_endpoint_does_not_query_notifications_table(
client_request,
service_one,
active_user_with_permissions,
mock_get_service_data_retention,
mocker,
fake_uuid,
):
"""Critical regression test: ensure poll status endpoint never queries notifications"""
mock_job = mocker.patch("app.job_api_client.get_job")
mock_job.return_value = {
"data": {
**job_json(
service_one["id"],
created_by=user_json(),
job_id=fake_uuid,
job_status="sending",
notification_count=500,
notifications_requested=500,
),
"statistics": [
{"status": "delivered", "count": 300},
{"status": "failed", "count": 50},
{"status": "pending", "count": 150},
],
}
}
mock_get_notifications = mocker.patch(
"app.notification_api_client.get_notifications_for_service"
)
response = client_request.get_response(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
)
assert response.status_code == 200
# Verify no notifications were fetched
mock_get_notifications.assert_not_called()
data = json.loads(response.get_data(as_text=True))
assert data["total_count"] == 500
assert data["sent_count"] == 300

View File

@@ -1533,9 +1533,7 @@ def test_should_be_able_to_move_to_new_folder(
],
},
_expected_status=302,
_expected_redirect=url_for(
"main.choose_template", service_id=SERVICE_ONE_ID
),
_expected_redirect=url_for("main.choose_template", service_id=SERVICE_ONE_ID),
)
mock_create_template_folder.assert_called_once_with(

View File

@@ -242,6 +242,7 @@ EXCLUDED_ENDPOINTS = tuple(
"verify_email",
"view_job",
"view_job_csv",
"view_job_status_poll",
"view_job_updates",
"view_jobs",
"view_notification",