Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix next_occurrence function #1893

Merged
8 changes: 7 additions & 1 deletion data_safe_haven/functions/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ def next_occurrence(
minute=minute,
second=0,
microsecond=0,
) + datetime.timedelta(days=1)
)
utc_dt = local_dt.astimezone(pytz.utc)
# Add one day until this datetime is at least 1 hour in the future.
# This ensures that any Azure functions which depend on this datetime being in
# the future should treat it as valid.
utc_near_future = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)
while utc_dt < utc_near_future:
utc_dt += datetime.timedelta(days=1)
if time_format == "iso":
return utc_dt.isoformat()
elif time_format == "iso_minute":
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ all = [
[tool.hatch.envs.test]
dependencies = [
"coverage>=7.5.1",
"freezegun>=1.5",
"pytest>=8.1",
"pytest-mock>=3.14",
]
Expand Down
97 changes: 50 additions & 47 deletions tests/functions/test_strings.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,59 @@
import datetime
import re

import pytest
import pytz
from freezegun import freeze_time

from data_safe_haven.exceptions import DataSafeHavenInputError
from data_safe_haven.functions import next_occurrence, sanitise_sre_name


def test_next_occurrence_is_within_next_day():
next_time = next_occurrence(5, 13, "Australia/Perth")
dt_next_time = datetime.datetime.fromisoformat(next_time)
dt_utc_now = datetime.datetime.now(datetime.UTC)
assert dt_next_time > dt_utc_now
assert dt_next_time < dt_utc_now + datetime.timedelta(days=1)


def test_next_occurrence_has_correct_time():
next_time = next_occurrence(5, 13, "Australia/Perth")
dt_as_utc = datetime.datetime.fromisoformat(next_time)
dt_as_local = dt_as_utc.astimezone(pytz.timezone("Australia/Perth"))
assert dt_as_local.hour == 5
assert dt_as_local.minute == 13


def test_next_occurrence_timeformat():
next_time = next_occurrence(5, 13, "Australia/Perth", time_format="iso_minute")
assert re.match(r"\d\d\d\d-\d\d-\d\d \d\d:13", next_time)


def test_next_occurrence_invalid_hour():
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(99, 13, "Europe/London")
assert exc_info.match(r"Time '99:13' was not recognised.")


def test_next_occurrence_invalid_minute():
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(5, 99, "Europe/London")
assert exc_info.match(r"Time '5:99' was not recognised.")


def test_next_occurrence_invalid_timezone():
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(5, 13, "Mars/OlympusMons")
assert exc_info.match(r"Timezone 'Mars/OlympusMons' was not recognised.")


def test_next_occurrence_invalid_timeformat():
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(5, 13, "Australia/Perth", time_format="invalid")
assert exc_info.match(r"Time format 'invalid' was not recognised.")
class TestNextOccurance:
@pytest.mark.parametrize(
"hour,minute,timezone,expected",
[
(5, 13, "Australia/Perth", "2024-01-02T21:13:00+00:00"),
(0, 13, "Australia/Perth", "2024-01-02T16:13:00+00:00"),
(20, 13, "Australia/Perth", "2024-01-02T12:13:00+00:00"),
(20, 13, "Europe/London", "2024-01-02T20:13:00+00:00"),
],
)
@freeze_time("1am on Jan 2nd, 2024")
def test_next_occurrence(self, hour, minute, timezone, expected):
next_time = next_occurrence(hour, minute, timezone)
assert next_time == expected

@freeze_time("1am on July 2nd, 2024")
def test_dst(self):
next_time = next_occurrence(13, 5, "Europe/London")
assert next_time == "2024-07-02T12:05:00+00:00"

@freeze_time("1am on Jan 2nd, 2024")
def test_timeformat(self):
next_time = next_occurrence(5, 13, "Australia/Perth", time_format="iso_minute")
assert next_time == "2024-01-02 21:13"

@freeze_time("9pm on Jan 2nd, 2024")
def test_is_tomorrow(self):
next_time = next_occurrence(5, 13, "Australia/Perth")
assert next_time == "2024-01-03T21:13:00+00:00"

def test_invalid_hour(self):
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(99, 13, "Europe/London")
assert exc_info.match(r"Time '99:13' was not recognised.")

def test_invalid_minute(self):
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(5, 99, "Europe/London")
assert exc_info.match(r"Time '5:99' was not recognised.")

def test_invalid_timezone(self):
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(5, 13, "Mars/OlympusMons")
assert exc_info.match(r"Timezone 'Mars/OlympusMons' was not recognised.")

def test_invalid_timeformat(self):
with pytest.raises(DataSafeHavenInputError) as exc_info:
next_occurrence(5, 13, "Australia/Perth", time_format="invalid")
assert exc_info.match(r"Time format 'invalid' was not recognised.")


@pytest.mark.parametrize(
Expand Down
Loading