mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-26 09:13:58 -04:00
Pointed new endpoint to new backend polling endpoint
This commit is contained in:
@@ -199,7 +199,7 @@ def create_app(application):
|
|||||||
|
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"FEATURE_SOCKET_ENABLED value in __init__.py coming \
|
f"FEATURE_SOCKET_ENABLED value in __init__.py coming \
|
||||||
from config is {application.config.get('FEATURED_SOCKET_ENABLED')} and \
|
from config is {application.config.get('FEATURE_SOCKET_ENABLED')} and \
|
||||||
the ending value is {feature_socket_enabled}"
|
the ending value is {feature_socket_enabled}"
|
||||||
)
|
)
|
||||||
return dict(
|
return dict(
|
||||||
|
|||||||
@@ -9,14 +9,20 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
// Extract job info from URL path: /services/{serviceId}/jobs/{jobId}
|
// Extract job info from URL path: /services/{serviceId}/jobs/{jobId}
|
||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
if (pathParts.length < 5 || pathParts[1] !== 'services' || pathParts[3] !== 'jobs') return;
|
if (
|
||||||
|
pathParts.length < 5 ||
|
||||||
|
pathParts[1] !== 'services' ||
|
||||||
|
pathParts[3] !== 'jobs'
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const serviceId = pathParts[2];
|
const serviceId = pathParts[2];
|
||||||
const jobId = pathParts[4];
|
const jobId = pathParts[4];
|
||||||
|
|
||||||
// Validate service and job IDs to prevent path injection
|
// Validate service and job IDs to prevent path injection
|
||||||
function isValidUuid(id) {
|
function isValidUuid(id) {
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
const uuidRegex =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
return uuidRegex.test(id);
|
return uuidRegex.test(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +46,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
MAX_INTERVAL_MS,
|
MAX_INTERVAL_MS,
|
||||||
Math.max(
|
Math.max(
|
||||||
MIN_INTERVAL_MS,
|
MIN_INTERVAL_MS,
|
||||||
Math.floor((250 * Math.sqrt(responseTime)) - 1000)
|
Math.floor(250 * Math.sqrt(responseTime) - 1000),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +63,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Update notifications container if it exists
|
// Update notifications container if it exists
|
||||||
const notificationsContainer = document.querySelector('[data-key="notifications"]');
|
const notificationsContainer = document.querySelector(
|
||||||
|
'[data-key="notifications"]',
|
||||||
|
);
|
||||||
if (notificationsContainer && data.notifications) {
|
if (notificationsContainer && data.notifications) {
|
||||||
notificationsContainer.innerHTML = data.notifications;
|
notificationsContainer.innerHTML = data.notifications;
|
||||||
}
|
}
|
||||||
@@ -87,7 +95,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const countsContainer = document.querySelector('[data-key="counts"]');
|
const countsContainer = document.querySelector('[data-key="counts"]');
|
||||||
if (countsContainer) {
|
if (countsContainer) {
|
||||||
// Get all big-number elements in order: total, pending, delivered, failed
|
// Get all big-number elements in order: total, pending, delivered, failed
|
||||||
const countElements = countsContainer.querySelectorAll('.big-number-number');
|
const countElements =
|
||||||
|
countsContainer.querySelectorAll('.big-number-number');
|
||||||
|
|
||||||
if (countElements.length >= 4) {
|
if (countElements.length >= 4) {
|
||||||
if (data.total_count !== undefined) {
|
if (data.total_count !== undefined) {
|
||||||
@@ -116,7 +125,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
// Update notifications conditionally:
|
// Update notifications conditionally:
|
||||||
// 1. If we have new messages and still under 50 total
|
// 1. If we have new messages and still under 50 total
|
||||||
// 2. Always when job is finished
|
// 2. Always when job is finished
|
||||||
if (processedCount > lastProcessedCount && processedCount <= 50 && !data.finished) {
|
if (
|
||||||
|
processedCount > lastProcessedCount &&
|
||||||
|
processedCount <= 50 &&
|
||||||
|
!data.finished
|
||||||
|
) {
|
||||||
// Update notifications for first 50 messages to show early results
|
// Update notifications for first 50 messages to show early results
|
||||||
await updateNotifications();
|
await updateNotifications();
|
||||||
lastProcessedCount = processedCount;
|
lastProcessedCount = processedCount;
|
||||||
@@ -126,7 +139,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
await updateNotifications();
|
await updateNotifications();
|
||||||
stopPolling();
|
stopPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (retryCount < 3) {
|
if (retryCount < 3) {
|
||||||
console.debug(`Job polling retry ${retryCount}`, error.message);
|
console.debug(`Job polling retry ${retryCount}`, error.message);
|
||||||
@@ -143,7 +155,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
url: pollStatusUrl,
|
url: pollStatusUrl,
|
||||||
jobId: jobId,
|
jobId: jobId,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
currentInterval = Math.min(currentInterval * 2, MAX_INTERVAL_MS);
|
currentInterval = Math.min(currentInterval * 2, MAX_INTERVAL_MS);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
@@ -21,6 +22,7 @@ from markupsafe import Markup
|
|||||||
from app import (
|
from app import (
|
||||||
current_service,
|
current_service,
|
||||||
format_datetime_table,
|
format_datetime_table,
|
||||||
|
job_api_client,
|
||||||
notification_api_client,
|
notification_api_client,
|
||||||
service_api_client,
|
service_api_client,
|
||||||
)
|
)
|
||||||
@@ -111,35 +113,42 @@ def cancel_job(service_id, job_id):
|
|||||||
@user_has_permissions()
|
@user_has_permissions()
|
||||||
def view_job_status_poll(service_id, job_id):
|
def view_job_status_poll(service_id, job_id):
|
||||||
"""
|
"""
|
||||||
Poll status endpoint that only queries jobs table.
|
Poll status endpoint using new lightweight cached API endpoint.
|
||||||
Returns minimal data needed for polling.
|
Returns minimal data needed for polling.
|
||||||
"""
|
"""
|
||||||
import time
|
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
job = Job.from_id(job_id, service_id=service_id)
|
# Use new lightweight status endpoint
|
||||||
|
try:
|
||||||
|
status_data = job_api_client.get_job_status(service_id, job_id)
|
||||||
|
|
||||||
processed_count = job.notifications_delivered + job.notifications_failed
|
# Validate the response has expected fields
|
||||||
total_count = job.notification_count
|
required_fields = ["sent_count", "failed_count", "pending_count", "total_count", "processing_finished"]
|
||||||
|
if all(key in status_data for key in required_fields):
|
||||||
|
response_data = {
|
||||||
|
"sent_count": status_data["sent_count"],
|
||||||
|
"failed_count": status_data["failed_count"],
|
||||||
|
"pending_count": status_data["pending_count"],
|
||||||
|
"total_count": status_data["total_count"],
|
||||||
|
"finished": status_data["processing_finished"],
|
||||||
|
}
|
||||||
|
processed_count = status_data["sent_count"] + status_data["failed_count"]
|
||||||
|
else:
|
||||||
|
current_app.logger.error(f"Status endpoint returned invalid response for job {job_id[:8]}: {status_data}")
|
||||||
|
abort(500, "Invalid status response from API")
|
||||||
|
|
||||||
response_data = {
|
except Exception as e:
|
||||||
"sent_count": job.notifications_delivered,
|
current_app.logger.error(f"Status endpoint failed for job {job_id[:8]}: {e}")
|
||||||
"failed_count": job.notifications_failed,
|
abort(500, "Status endpoint unavailable")
|
||||||
"pending_count": job.notifications_sending,
|
|
||||||
"total_count": total_count,
|
|
||||||
"finished": job.finished_processing,
|
|
||||||
}
|
|
||||||
|
|
||||||
response_time_ms = round((time.time() - start_time) * 1000, 2)
|
response_time_ms = round((time.time() - start_time) * 1000, 2)
|
||||||
response_json = json.dumps(response_data)
|
response_json = json.dumps(response_data)
|
||||||
response_size_bytes = len(response_json.encode("utf-8"))
|
response_size_bytes = len(response_json.encode("utf-8"))
|
||||||
|
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"Poll status request - job_id={job_id[:8]} "
|
f"Poll status request - job_id={job_id[:8]} "
|
||||||
f"response_size={response_size_bytes}b "
|
f"response_size={response_size_bytes}b "
|
||||||
f"response_time={response_time_ms}ms "
|
f"response_time={response_time_ms}ms "
|
||||||
f"progress={processed_count}/{total_count}"
|
f"progress={processed_count}/{response_data['total_count']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify(response_data)
|
return jsonify(response_data)
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class JobApiClient(NotifyAdminAPIClient):
|
|||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
def get_job_status(self, service_id, job_id):
|
||||||
|
return self.get(url=f"/service/{service_id}/job/{job_id}/status")
|
||||||
|
|
||||||
def get_jobs(
|
def get_jobs(
|
||||||
self,
|
self,
|
||||||
service_id,
|
service_id,
|
||||||
|
|||||||
@@ -16,7 +16,14 @@ from tests import job_json, user_json
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("delivered", "failed", "pending", "finished", "js_should_update_notifications", "reason"),
|
(
|
||||||
|
"delivered",
|
||||||
|
"failed",
|
||||||
|
"pending",
|
||||||
|
"finished",
|
||||||
|
"js_should_update_notifications",
|
||||||
|
"reason",
|
||||||
|
),
|
||||||
[
|
[
|
||||||
(20, 10, 70, False, True, "30 messages processed (≤50 threshold)"),
|
(20, 10, 70, False, True, "30 messages processed (≤50 threshold)"),
|
||||||
(40, 10, 50, False, True, "50 messages processed (exactly at threshold)"),
|
(40, 10, 50, False, True, "50 messages processed (exactly at threshold)"),
|
||||||
@@ -134,7 +141,13 @@ def test_poll_status_provides_required_fields(
|
|||||||
|
|
||||||
data = json.loads(response.get_data(as_text=True))
|
data = json.loads(response.get_data(as_text=True))
|
||||||
|
|
||||||
required_fields = {"sent_count", "failed_count", "finished", "pending_count", "total_count"}
|
required_fields = {
|
||||||
|
"sent_count",
|
||||||
|
"failed_count",
|
||||||
|
"finished",
|
||||||
|
"pending_count",
|
||||||
|
"total_count",
|
||||||
|
}
|
||||||
assert set(data.keys()) == required_fields
|
assert set(data.keys()) == required_fields
|
||||||
|
|
||||||
response_size = len(response.get_data(as_text=True))
|
response_size = len(response.get_data(as_text=True))
|
||||||
|
|||||||
@@ -510,23 +510,13 @@ def test_poll_status_endpoint(
|
|||||||
fake_uuid,
|
fake_uuid,
|
||||||
):
|
):
|
||||||
"""Test that the poll status endpoint returns only required data without notifications"""
|
"""Test that the poll status endpoint returns only required data without notifications"""
|
||||||
mock_job = mocker.patch("app.job_api_client.get_job")
|
mock_job_status = mocker.patch("app.job_api_client.get_job_status")
|
||||||
mock_job.return_value = {
|
mock_job_status.return_value = {
|
||||||
"data": {
|
"sent_count": 90,
|
||||||
**job_json(
|
"failed_count": 10,
|
||||||
service_one["id"],
|
"pending_count": 0,
|
||||||
created_by=user_json(),
|
"total_count": 100,
|
||||||
job_id=fake_uuid,
|
"processing_finished": True,
|
||||||
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(
|
response = client_request.get_response(
|
||||||
@@ -563,19 +553,13 @@ def test_poll_status_with_zero_notifications(
|
|||||||
fake_uuid,
|
fake_uuid,
|
||||||
):
|
):
|
||||||
"""Test poll status endpoint handles edge case of no notifications"""
|
"""Test poll status endpoint handles edge case of no notifications"""
|
||||||
mock_job = mocker.patch("app.job_api_client.get_job")
|
mock_job_status = mocker.patch("app.job_api_client.get_job_status")
|
||||||
mock_job.return_value = {
|
mock_job_status.return_value = {
|
||||||
"data": {
|
"sent_count": 0,
|
||||||
**job_json(
|
"failed_count": 0,
|
||||||
service_one["id"],
|
"pending_count": 0,
|
||||||
created_by=user_json(),
|
"total_count": 0,
|
||||||
job_id=fake_uuid,
|
"processing_finished": True,
|
||||||
job_status="pending",
|
|
||||||
notification_count=0,
|
|
||||||
notifications_requested=0,
|
|
||||||
),
|
|
||||||
"statistics": [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client_request.get_response(
|
response = client_request.get_response(
|
||||||
@@ -588,9 +572,7 @@ def test_poll_status_with_zero_notifications(
|
|||||||
data = json.loads(response.get_data(as_text=True))
|
data = json.loads(response.get_data(as_text=True))
|
||||||
|
|
||||||
assert data["total_count"] == 0
|
assert data["total_count"] == 0
|
||||||
assert (
|
assert data["finished"] is True
|
||||||
data["finished"] is True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_poll_status_endpoint_does_not_query_notifications_table(
|
def test_poll_status_endpoint_does_not_query_notifications_table(
|
||||||
@@ -602,23 +584,13 @@ def test_poll_status_endpoint_does_not_query_notifications_table(
|
|||||||
fake_uuid,
|
fake_uuid,
|
||||||
):
|
):
|
||||||
"""Critical regression test: ensure poll status endpoint never queries notifications"""
|
"""Critical regression test: ensure poll status endpoint never queries notifications"""
|
||||||
mock_job = mocker.patch("app.job_api_client.get_job")
|
mock_job_status = mocker.patch("app.job_api_client.get_job_status")
|
||||||
mock_job.return_value = {
|
mock_job_status.return_value = {
|
||||||
"data": {
|
"sent_count": 300,
|
||||||
**job_json(
|
"failed_count": 50,
|
||||||
service_one["id"],
|
"pending_count": 150,
|
||||||
created_by=user_json(),
|
"total_count": 500,
|
||||||
job_id=fake_uuid,
|
"processing_finished": False,
|
||||||
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(
|
mock_get_notifications = mocker.patch(
|
||||||
@@ -639,3 +611,6 @@ def test_poll_status_endpoint_does_not_query_notifications_table(
|
|||||||
data = json.loads(response.get_data(as_text=True))
|
data = json.loads(response.get_data(as_text=True))
|
||||||
assert data["total_count"] == 500
|
assert data["total_count"] == 500
|
||||||
assert data["sent_count"] == 300
|
assert data["sent_count"] == 300
|
||||||
|
assert data["failed_count"] == 50
|
||||||
|
assert data["pending_count"] == 150
|
||||||
|
assert data["finished"] is False
|
||||||
|
|||||||
Reference in New Issue
Block a user