Optimizing polling

This commit is contained in:
Alex Janousek
2025-09-25 16:26:00 -04:00
parent 8b76e45049
commit 557d85f57d
15 changed files with 523 additions and 163 deletions

View File

@@ -0,0 +1,136 @@
"""
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:
assert (processed_count <= 50 and not finished) or finished, \
f"JS updates notifications: {reason}"
else:
assert processed_count > 50 and 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",