From 77f733cbf935e3d442f549b5986f79fd042dede1 Mon Sep 17 00:00:00 2001 From: stuppie Date: Wed, 4 Nov 2026 14:45:25 -0600 Subject: implement active hours on shared Task model --- tests/managers/thl/test_survey.py | 33 ++++++++- tests/models/thl/test_survey.py | 140 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 tests/models/thl/test_survey.py (limited to 'tests') diff --git a/tests/managers/thl/test_survey.py b/tests/managers/thl/test_survey.py index e114b70..928fe6c 100644 --- a/tests/managers/thl/test_survey.py +++ b/tests/managers/thl/test_survey.py @@ -2,7 +2,7 @@ from __future__ import annotations import uuid from collections.abc import Callable -from datetime import UTC, datetime +from datetime import UTC, datetime, time from decimal import Decimal from typing import TYPE_CHECKING @@ -23,6 +23,7 @@ from generalresearch.models.thl.survey.model import ( SurveyCategoryModel, SurveyEligibilityDefinition, SurveyStat, + TimeInterval, ) if TYPE_CHECKING: @@ -112,6 +113,36 @@ class TestSurvey: assert res2[0] == res[0] assert len(res2) == len(surveys2) + def test_active_hours_round_trip( + self, + delete_buyers_surveys: Callable[..., None], + survey_manager: SurveyManager, + ): + delete_buyers_surveys() + active_hours = [ + TimeInterval(start=time(5), end=time(1), timezone=None), + TimeInterval( + start=time(8), + end=time(17), + timezone="America/New_York", + ), + ] + survey = Survey( + source=Source.TESTING, + survey_id="active-hours", + buyer_code="active-hours-buyer", + active_hours=active_hours, + ) + + survey_manager.create_or_update([survey]) + result = survey_manager.filter_by_natural_key( + source=survey.source, + survey_ids=[survey.survey_id], + ) + + assert len(result) == 1 + assert result[0].active_hours == active_hours + def test_category(self, survey_manager: SurveyManager): survey1 = Survey(id=562289, survey_id="a", source=Source.TESTING) survey2 = Survey(id=562290, survey_id="a", source=Source.TESTING) diff --git a/tests/models/thl/test_survey.py b/tests/models/thl/test_survey.py new file mode 100644 index 0000000..b283e50 --- /dev/null +++ b/tests/models/thl/test_survey.py @@ -0,0 +1,140 @@ +from datetime import UTC, datetime, time +from unittest.mock import patch +from zoneinfo import ZoneInfo + +import pytest + +from generalresearch.models.definitions import Source +from generalresearch.models.thl.survey.model import Survey, TimeInterval + + +@pytest.mark.parametrize( + ("hour", "minute", "expected"), + [ + (6, 59, False), + (7, 0, True), + (12, 0, True), + (22, 0, True), + (22, 1, False), + ], +) +def test_is_currently_active_in_worker_local_time(hour, minute, expected): + interval = TimeInterval(start=time(7), end=time(22), timezone=None) + worker_time = datetime( + 2026, 1, 15, hour, minute, tzinfo=ZoneInfo("America/Chicago") + ) + + assert interval.is_currently_active(worker_time) is expected + + +@pytest.mark.parametrize( + ("hour", "expected"), + [ + (21, False), + (22, True), + (23, True), + (0, True), + (6, True), + (7, False), + ], +) +def test_is_currently_active_for_interval_crossing_midnight(hour, expected): + interval = TimeInterval(start=time(22), end=time(6), timezone=None) + worker_time = datetime(2026, 1, 15, hour, tzinfo=UTC) + + assert interval.is_currently_active(worker_time) is expected + + +def test_is_currently_active_converts_to_interval_timezone(): + interval = TimeInterval( + start=time(7), + end=time(9), + timezone="America/New_York", + ) + # 08:00 in New York on this date. + worker_time = datetime( + 2026, 1, 15, 7, tzinfo=ZoneInfo("America/Chicago") + ) + + assert interval.is_currently_active(worker_time) is True + + +def test_is_currently_active_requires_aware_worker_time(): + interval = TimeInterval(start=time(7), end=time(22), timezone=None) + + with pytest.raises(AssertionError): + interval.is_currently_active(datetime(2026, 1, 15, 12)) + + +def test_is_currently_active_requires_worker_timezone_for_local_interval(): + interval = TimeInterval(start=time(7), end=time(22), timezone=None) + + assert interval.is_currently_active_by_worker_tz(worker_tz=None) is False + + +@pytest.mark.parametrize( + ("interval_timezone", "worker_timezone"), + [ + (None, ZoneInfo("America/Chicago")), + ("America/New_York", None), + ], +) +def test_is_currently_active_supports_interval_crossing_midnight( + interval_timezone, worker_timezone +): + interval = TimeInterval( + start=time(22), + end=time(6), + timezone=interval_timezone, + ) + current_timezone = ( + ZoneInfo(interval_timezone) if interval_timezone else worker_timezone + ) + current_time = datetime(2026, 1, 15, 23, tzinfo=current_timezone) + + with patch("generalresearch.models.thl.survey.model.datetime") as mock_datetime: + mock_datetime.now.return_value = current_time + + assert interval.is_currently_active_by_worker_tz(worker_timezone) is True + mock_datetime.now.assert_called_once_with(tz=current_timezone) + + +@pytest.mark.parametrize( + ("off_start", "off_end", "active_start", "active_end"), + [ + (time(1), time(5), time(5), time(1)), + (time(22), time(6), time(6), time(22)), + ], +) +def test_from_off_hours_returns_complementary_active_hours( + off_start, off_end, active_start, active_end +): + off_hours = TimeInterval( + start=off_start, + end=off_end, + timezone="America/New_York", + ) + + active_hours = Survey.active_hours_from_off_hours(off_hours) + + assert active_hours == [ + TimeInterval( + start=active_start, + end=active_end, + timezone="America/New_York", + ) + ] + + +def test_from_off_hours_result_can_be_assigned_to_survey(): + off_hours = TimeInterval(start=time(1), end=time(5), timezone=None) + + survey = Survey( + source=Source.TESTING, + survey_id="off-hours-test", + active_hours=Survey.active_hours_from_off_hours(off_hours), + ) + + assert survey.active_hours == [ + TimeInterval(start=time(5), end=time(1), timezone=None) + ] -- cgit v1.2.3