mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-07-12 17:34:16 -04:00
Added unit tests for new utc conversion logic
This commit is contained in:
5
Makefile
5
Makefile
@@ -162,3 +162,8 @@ upload-static:
|
||||
# @cf map-route notify-admin ${DNS_NAME} --hostname www
|
||||
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
|
||||
# @echo "Failwhale is disabled"
|
||||
|
||||
.PHONY: test-single
|
||||
test-single: export NEW_RELIC_ENVIRONMENT=test
|
||||
test-single: ## Run a single test file
|
||||
poetry run pytest $(TEST_FILE)
|
||||
|
||||
@@ -213,7 +213,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var url = type === 'service' ? `/services/${currentServiceId}/daily-stats.json` : `/services/${currentServiceId}/daily-stats-by-user.json`;
|
||||
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
var url = type === 'service'
|
||||
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
|
||||
: `/services/${currentServiceId}/daily-stats-by-user.json`;
|
||||
|
||||
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import calendar
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from flask import Response, abort, jsonify, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
@@ -80,10 +81,47 @@ def service_dashboard(service_id):
|
||||
@user_has_permissions()
|
||||
def get_daily_stats(service_id):
|
||||
date_range = get_stats_date_range()
|
||||
stats = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||
days = date_range["days"]
|
||||
user_timezone = request.args.get("timezone", "UTC")
|
||||
|
||||
stats_utc = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
start_date=date_range["start_date"],
|
||||
days=days,
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
|
||||
return jsonify(local_stats)
|
||||
|
||||
|
||||
def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
|
||||
tz = ZoneInfo(user_timezone)
|
||||
today_local = datetime.now(tz).date()
|
||||
start_local = today_local - timedelta(days=days - 1)
|
||||
|
||||
# Generate exactly days local dates, each with zeroed stats
|
||||
days_list = [
|
||||
(start_local + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(days)
|
||||
]
|
||||
aggregator = {
|
||||
d: {
|
||||
"sms": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
}
|
||||
for d in days_list
|
||||
}
|
||||
|
||||
# Convert each UTC timestamp to local date and iterate
|
||||
for utc_ts, data in stats_utc.items():
|
||||
utc_dt = datetime.strptime(utc_ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=ZoneInfo("UTC"))
|
||||
local_day = utc_dt.astimezone(tz).strftime("%Y-%m-%d")
|
||||
|
||||
if local_day in aggregator:
|
||||
for msg_type in ["sms", "email"]:
|
||||
for status in ["delivered", "failure", "pending", "requested"]:
|
||||
aggregator[local_day][msg_type][status] += data[msg_type][status]
|
||||
|
||||
return aggregator
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/daily-stats-by-user.json")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import copy
|
||||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
from flask import url_for
|
||||
@@ -12,6 +14,7 @@ from app.main.views.dashboard import (
|
||||
aggregate_template_usage,
|
||||
format_monthly_stats_to_list,
|
||||
get_dashboard_totals,
|
||||
get_local_daily_stats_for_last_x_days,
|
||||
get_tuples_of_financial_years,
|
||||
)
|
||||
from tests import (
|
||||
@@ -1945,3 +1948,155 @@ def test_service_dashboard_shows_batched_jobs(
|
||||
assert len(rows) == 1
|
||||
|
||||
assert job_table_body is not None
|
||||
|
||||
|
||||
@patch("app.main.views.dashboard.datetime", wraps=datetime)
|
||||
def test_simple_local_conversion(mock_dt):
|
||||
stats_utc = {
|
||||
"2025-02-24T15:00:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-25T07:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
}
|
||||
|
||||
# Mock today's date in local time: 2025-02-25 at 10:00
|
||||
mock_dt.now.return_value = datetime(2025, 2, 25, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))
|
||||
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc,
|
||||
"America/New_York",
|
||||
days=2
|
||||
)
|
||||
|
||||
assert len(local_stats) == 2
|
||||
assert "2025-02-24" in local_stats
|
||||
assert "2025-02-25" in local_stats
|
||||
|
||||
assert local_stats["2025-02-24"]["sms"]["delivered"] == 1
|
||||
assert local_stats["2025-02-24"]["sms"]["requested"] == 1
|
||||
assert local_stats["2025-02-25"]["sms"]["delivered"] == 2
|
||||
assert local_stats["2025-02-25"]["sms"]["requested"] == 2
|
||||
|
||||
|
||||
def test_no_timestamps_returns_zeroed_days():
|
||||
stats_utc = {}
|
||||
|
||||
class MockDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return cls(2025, 2, 26, 8, 0, 0, tzinfo=tz)
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", MockDateTime):
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc, "America/New_York", days=3
|
||||
)
|
||||
assert list(local_stats.keys()) == ["2025-02-24", "2025-02-25", "2025-02-26"]
|
||||
for day in local_stats:
|
||||
for msg_type in ["sms", "email"]:
|
||||
for status in ["delivered", "failure", "pending", "requested"]:
|
||||
assert local_stats[day][msg_type][status] == 0
|
||||
|
||||
|
||||
def test_timestamp_in_future_time_zone():
|
||||
stats_utc = {
|
||||
"2025-02-25T01:00:00Z": {
|
||||
"sms": {"delivered": 5, "failure": 0, "pending": 0, "requested": 5},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
}
|
||||
}
|
||||
|
||||
class MockDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return cls(2025, 2, 25, 10, 0, 0, tzinfo=tz)
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", MockDateTime):
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc, "Asia/Shanghai", days=1
|
||||
)
|
||||
|
||||
assert list(local_stats.keys()) == ["2025-02-25"]
|
||||
assert local_stats["2025-02-25"]["sms"]["delivered"] == 5
|
||||
|
||||
|
||||
def test_many_timestamps_one_local_day():
|
||||
stats_utc = {
|
||||
"2025-02-24T05:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 1, "pending": 0, "requested": 3},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-24T09:30:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
},
|
||||
"2025-02-24T16:59:59Z": {
|
||||
"sms": {"delivered": 4, "failure": 0, "pending": 0, "requested": 4},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
}
|
||||
|
||||
class MockDateTime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return cls(2025, 2, 24, 20, 0, 0, tzinfo=tz)
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", MockDateTime):
|
||||
local_stats = get_local_daily_stats_for_last_x_days(
|
||||
stats_utc, "America/New_York", days=1
|
||||
)
|
||||
|
||||
assert list(local_stats.keys()) == ["2025-02-24"]
|
||||
assert local_stats["2025-02-24"]["sms"]["delivered"] == 7
|
||||
assert local_stats["2025-02-24"]["sms"]["requested"] == 8
|
||||
assert local_stats["2025-02-24"]["email"]["delivered"] == 2
|
||||
assert local_stats["2025-02-24"]["email"]["requested"] == 2
|
||||
|
||||
|
||||
def test_local_conversion_phoenix():
|
||||
"""Test aggregator logic in Mountain Time, no DST (America/Phoenix)."""
|
||||
stats_utc = {
|
||||
"2025-02-25T01:00:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-25T12:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
"email": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
},
|
||||
}
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
|
||||
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("America/Phoenix"))
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "America/Phoenix", days=1)
|
||||
|
||||
assert len(local_stats) == 1
|
||||
(day_key,) = local_stats.keys()
|
||||
sms_delivered = local_stats[day_key]["sms"]["delivered"]
|
||||
assert sms_delivered in (2, 3, 4)
|
||||
|
||||
|
||||
def test_local_conversion_honolulu():
|
||||
"""Test aggregator logic in Hawaii (Pacific/Honolulu)."""
|
||||
stats_utc = {
|
||||
"2025-02-25T03:00:00Z": {
|
||||
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
||||
},
|
||||
"2025-02-25T21:00:00Z": {
|
||||
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
|
||||
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
|
||||
},
|
||||
}
|
||||
|
||||
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
|
||||
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("Pacific/Honolulu"))
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "Pacific/Honolulu", days=1)
|
||||
|
||||
assert len(local_stats) == 1
|
||||
(day_key,) = local_stats.keys()
|
||||
total_requested = local_stats[day_key]["sms"]["requested"]
|
||||
assert total_requested in (3, 1, 4)
|
||||
|
||||
Reference in New Issue
Block a user