screen-locker/screen_locker/tests/test_weekly_check.py

244 lines
8.6 KiB
Python
Raw Permalink Normal View History

"""Tests for _weekly_check: is_relaxed_day, count_weekly_workouts,
has_weekly_minimum."""
from __future__ import annotations
from datetime import datetime, timezone
import json
from typing import TYPE_CHECKING, Any
from unittest.mock import patch
from screen_locker._weekly_check import (
_RELAXED_WEEKDAYS,
WEEKLY_WORKOUT_MINIMUM,
count_weekly_workouts,
has_weekly_minimum,
is_relaxed_day,
)
if TYPE_CHECKING:
from pathlib import Path
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _dt(weekday: int, hour: int = 10) -> datetime:
"""Return a UTC-aware datetime for the given ISO weekday (0=Mon, 6=Sun)."""
# 2025-05-19 is a Monday (weekday 0)
base = datetime(2025, 5, 19, hour, 0, 0, tzinfo=timezone.utc)
from datetime import timedelta
return base + timedelta(days=weekday)
def _make_log(entries: dict[str, str], log_file: Path) -> Path:
"""Write a workout_log.json with given date→workout_type mapping."""
data: dict[str, Any] = {
date: {
"timestamp": f"{date}T10:00:00+00:00",
"workout_data": {"type": wtype},
}
for date, wtype in entries.items()
}
log_file.write_text(json.dumps(data))
return log_file
# ---------------------------------------------------------------------------
# is_relaxed_day
# ---------------------------------------------------------------------------
class TestIsRelaxedDay:
def test_monday_is_enforced(self) -> None:
assert is_relaxed_day(today=_dt(0)) is False
def test_tuesday_is_relaxed(self) -> None:
assert is_relaxed_day(today=_dt(1)) is True
def test_wednesday_is_relaxed(self) -> None:
assert is_relaxed_day(today=_dt(2)) is True
def test_thursday_is_relaxed(self) -> None:
assert is_relaxed_day(today=_dt(3)) is True
def test_friday_is_enforced(self) -> None:
assert is_relaxed_day(today=_dt(4)) is False
def test_saturday_is_enforced(self) -> None:
assert is_relaxed_day(today=_dt(5)) is False
def test_sunday_is_enforced(self) -> None:
assert is_relaxed_day(today=_dt(6)) is False
def test_relaxed_weekdays_constant_correct(self) -> None:
assert frozenset({1, 2, 3}) == _RELAXED_WEEKDAYS
def test_uses_local_time_by_default(self) -> None:
result = is_relaxed_day()
assert isinstance(result, bool)
# ---------------------------------------------------------------------------
# count_weekly_workouts
# ---------------------------------------------------------------------------
class TestCountWeeklyWorkouts:
def test_no_log_file_returns_zero(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_corrupt_json_returns_zero(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
log.write_text("{not valid json}")
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_oserror_returns_zero(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
log.write_text("{}")
with patch("builtins.open", side_effect=OSError("no permission")):
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_counts_phone_verified_in_current_week(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
# Mon=2025-05-19, Tue=2025-05-20 both in same week; check on Fri=2025-05-23
_make_log({"2025-05-19": "phone_verified", "2025-05-20": "phone_verified"}, log)
assert count_weekly_workouts(log, today=_dt(4)) == 2
def test_sick_day_not_counted(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log({"2025-05-19": "sick_day"}, log)
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_early_bird_not_counted(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log({"2025-05-19": "early_bird"}, log)
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_previous_week_not_counted(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
# 2025-05-12 is the Monday of the previous week
_make_log({"2025-05-12": "phone_verified"}, log)
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_future_date_not_counted(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
# 2025-05-24 is Saturday, checking on Friday 2025-05-23
_make_log({"2025-05-24": "phone_verified"}, log)
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_invalid_date_key_skipped(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
data: dict[str, Any] = {
"not-a-date": {
"timestamp": "x",
"workout_data": {"type": "phone_verified"},
},
"2025-05-19": {
"timestamp": "x",
"workout_data": {"type": "phone_verified"},
},
}
log.write_text(json.dumps(data))
assert count_weekly_workouts(log, today=_dt(4)) == 1
def test_non_dict_entry_skipped(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
data: dict[str, Any] = {"2025-05-19": "not-a-dict"}
log.write_text(json.dumps(data))
assert count_weekly_workouts(log, today=_dt(4)) == 0
def test_counts_up_to_four(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log(
{
"2025-05-19": "phone_verified",
"2025-05-20": "phone_verified",
"2025-05-21": "phone_verified",
"2025-05-22": "phone_verified",
},
log,
)
assert count_weekly_workouts(log, today=_dt(4)) == 4
def test_today_counts_if_this_week(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
# today is Friday 2025-05-23
_make_log({"2025-05-23": "phone_verified"}, log)
assert count_weekly_workouts(log, today=_dt(4)) == 1
def test_monday_start_of_week_counted(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log({"2025-05-19": "phone_verified"}, log)
# Checking on Monday itself (today=Mon)
assert count_weekly_workouts(log, today=_dt(0)) == 1
def test_mixed_types_only_verified_counted(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log(
{
"2025-05-19": "phone_verified",
"2025-05-20": "sick_day",
"2025-05-21": "early_bird",
"2025-05-22": "phone_verified",
},
log,
)
assert count_weekly_workouts(log, today=_dt(4)) == 2
# ---------------------------------------------------------------------------
# has_weekly_minimum
# ---------------------------------------------------------------------------
class TestHasWeeklyMinimum:
def test_zero_workouts_is_false(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
assert has_weekly_minimum(log, today=_dt(4)) is False
def test_three_workouts_is_false(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log(
{
"2025-05-19": "phone_verified",
"2025-05-20": "phone_verified",
"2025-05-21": "phone_verified",
},
log,
)
assert has_weekly_minimum(log, today=_dt(4)) is False
def test_four_workouts_is_true(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log(
{
"2025-05-19": "phone_verified",
"2025-05-20": "phone_verified",
"2025-05-21": "phone_verified",
"2025-05-22": "phone_verified",
},
log,
)
assert has_weekly_minimum(log, today=_dt(4)) is True
def test_five_workouts_is_true(self, tmp_path: Path) -> None:
log = tmp_path / "workout_log.json"
_make_log(
{
"2025-05-19": "phone_verified",
"2025-05-20": "phone_verified",
"2025-05-21": "phone_verified",
"2025-05-22": "phone_verified",
"2025-05-23": "phone_verified",
},
log,
)
assert has_weekly_minimum(log, today=_dt(4)) is True
def test_weekly_workout_minimum_constant(self) -> None:
assert WEEKLY_WORKOUT_MINIMUM == 4