screen-locker/screen_locker/tests/test_phone_verification_part4.py

210 lines
7.4 KiB
Python
Raw Normal View History

"""Tests for JSON workout validation and HTTP fallback (part 4).
Replaces the obsolete StrongLifts-DB-based ``test_phone_check_unlock.py``.
Covers ``_validate_json_data`` (all status branches) and the HTTP fallback
``_scan_for_http_server`` / ``_fetch_http_workout`` used when ADB is
unavailable. Network is fully mocked no test touches a real socket.
"""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from screen_locker._constants import MIN_WORKOUT_DURATION_MINUTES
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
def _today() -> str:
"""Return today's date as the validator computes it (local YYYY-MM-DD)."""
return time.strftime("%Y-%m-%d")
def _mock_cm(return_value: MagicMock) -> MagicMock:
"""Build a MagicMock usable as a context manager yielding ``return_value``."""
cm = MagicMock()
cm.__enter__ = MagicMock(return_value=return_value)
cm.__exit__ = MagicMock(return_value=False)
return cm
class TestValidateJsonData:
"""Tests for _validate_json_data across every status branch."""
def test_stale_when_not_today(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
status, message = locker._validate_json_data(
{"date": "2000-01-01", "exercises": ["x"], "duration_seconds": 4000}
)
assert status == "stale"
assert "2000-01-01" in message
def test_no_exercises_when_empty(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
status, message = locker._validate_json_data(
{"date": _today(), "exercises": [], "duration_seconds": 4000}
)
assert status == "no_exercises"
assert "exercise" in message.lower()
def test_too_short_under_minimum(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
short_seconds = int((MIN_WORKOUT_DURATION_MINUTES - 10) * 60)
status, message = locker._validate_json_data(
{"date": _today(), "exercises": ["x"], "duration_seconds": short_seconds}
)
assert status == "too_short"
assert f"{MIN_WORKOUT_DURATION_MINUTES}" in message
def test_verified_all_succeeded(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
status, message = locker._validate_json_data(
{
"date": _today(),
"exercises": ["x"],
"duration_seconds": 6000,
"succeeded": True,
}
)
assert status == "verified"
assert "all succeeded" in message
def test_verified_partial_when_not_succeeded(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
status, message = locker._validate_json_data(
{
"date": _today(),
"exercises": ["x"],
"duration_seconds": 6000,
"succeeded": False,
}
)
assert status == "verified"
assert "partial" in message
class TestScanForHttpServer:
"""Tests for _scan_for_http_server subnet probing."""
def test_returns_none_without_prefix(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
with patch.object(locker, "_get_local_subnet_prefix", return_value=None):
assert locker._scan_for_http_server() is None
def test_returns_url_when_probe_connects(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch(
"screen_locker._phone_verification.socket.create_connection",
return_value=_mock_cm(MagicMock()),
),
):
result = locker._scan_for_http_server()
assert result is not None
assert result.startswith("http://192.168.1.")
assert result.endswith(":8765/workout")
def test_returns_none_when_all_probes_refused(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch(
"screen_locker._phone_verification.socket.create_connection",
side_effect=OSError("refused"),
),
):
assert locker._scan_for_http_server() is None
class TestFetchHttpWorkout:
"""Tests for _fetch_http_workout over the local HTTP server."""
def test_returns_none_when_scan_finds_nothing(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
with patch.object(locker, "_scan_for_http_server", return_value=None):
assert locker._fetch_http_workout() is None
def test_returns_json_on_http_200(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
resp = MagicMock()
resp.status = 200
resp.read.return_value = b'{"date": "2026-06-12", "exercises": ["a"]}'
conn = MagicMock()
conn.getresponse.return_value = resp
with (
patch.object(
locker,
"_scan_for_http_server",
return_value="http://192.168.1.5:8765/workout",
),
patch(
"screen_locker._phone_verification._HTTPConnection",
return_value=conn,
),
):
result = locker._fetch_http_workout()
assert result == {"date": "2026-06-12", "exercises": ["a"]}
def test_returns_none_on_non_ok_status(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
resp = MagicMock()
resp.status = 404
conn = MagicMock()
conn.getresponse.return_value = resp
with (
patch.object(
locker,
"_scan_for_http_server",
return_value="http://192.168.1.5:8765/workout",
),
patch(
"screen_locker._phone_verification._HTTPConnection",
return_value=conn,
),
):
assert locker._fetch_http_workout() is None
def test_returns_none_on_connection_error(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(
locker,
"_scan_for_http_server",
return_value="http://192.168.1.5:8765/workout",
),
patch(
"screen_locker._phone_verification._HTTPConnection",
side_effect=OSError("unreachable"),
),
):
assert locker._fetch_http_workout() is None