screen-locker/screen_locker/tests/test_leetcode_bonus.py
Krzysztof kuhy Rudnicki 6a07dc56aa
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter-coverage (push) Waiting to run
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Reading hour from book-guard: +1h shutdown; base 20->19 from 2026-10-01
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 19:02:24 +02:00

196 lines
7.0 KiB
Python

"""Tests for the LeetCode shutdown hour: reading the ledger, failing closed.
Entries are signed against a temp key with gatelock's own ``compute_entry_hmac``
so a forged credit and a genuine one differ in exactly the way they do on the
machine.
"""
from __future__ import annotations
from datetime import datetime, timedelta
import json
import logging
from typing import TYPE_CHECKING, Any
from unittest.mock import patch
from gatelock.log_integrity import compute_entry_hmac
import pytest
from screen_locker import _leetcode_bonus
from screen_locker._leetcode_bonus import (
LEETCODE_BONUS_HOURS,
leetcode_bonus_hours,
leetcode_solved_today,
)
if TYPE_CHECKING:
from pathlib import Path
_NOW = datetime(2026, 9, 15, 14, 0).astimezone()
@pytest.fixture
def key_file(tmp_path: Path) -> Path:
key = tmp_path / "hmac.key"
key.write_bytes(b"test-key-bytes")
with patch.object(_leetcode_bonus, "HMAC_KEY_FILE", key):
yield key
def _entry(
kind: str, *, submitted_at: object, key: Path, day: str = "2026-09-15"
) -> dict[str, Any]:
body: dict[str, Any] = {
"entry_id": f"{kind}:{submitted_at}",
"kind": kind,
"day": day,
"detail": {"submitted_at": submitted_at},
}
return {**body, "hmac": compute_entry_hmac(body, key_file=key)}
def _ledger(tmp_path: Path, entries: list[Any]) -> Path:
path = tmp_path / "ledger.json"
path.write_text(json.dumps({"entries": entries}))
return path
class TestSolvedToday:
def test_verified_credit_today_is_solved(
self, tmp_path: Path, key_file: Path
) -> None:
stamp = (_NOW - timedelta(hours=2)).timestamp()
ledger = _ledger(tmp_path, [_entry("credit", submitted_at=stamp, key=key_file)])
assert leetcode_solved_today(ledger, now=_NOW) is True
def test_credit_from_yesterday_is_not_solved(
self, tmp_path: Path, key_file: Path
) -> None:
"""A 23:50 solve harvested this morning counts for yesterday, never today."""
stamp = (_NOW - timedelta(days=1)).timestamp()
ledger = _ledger(tmp_path, [_entry("credit", submitted_at=stamp, key=key_file)])
assert leetcode_solved_today(ledger, now=_NOW) is False
def test_seen_and_charge_entries_never_count(
self, tmp_path: Path, key_file: Path
) -> None:
stamp = _NOW.timestamp()
ledger = _ledger(
tmp_path,
[
_entry("seen", submitted_at=stamp, key=key_file),
_entry("charge", submitted_at=stamp, key=key_file),
"not-a-dict",
],
)
assert leetcode_solved_today(ledger, now=_NOW) is False
def test_forged_credit_is_ignored(self, tmp_path: Path, key_file: Path) -> None:
entry = _entry("credit", submitted_at=_NOW.timestamp(), key=key_file)
entry["detail"]["submitted_at"] = (
_NOW.timestamp() - 1
) # body no longer matches hmac
ledger = _ledger(tmp_path, [entry])
assert leetcode_solved_today(ledger, now=_NOW) is False
def test_unparsable_submitted_at_falls_back_to_day_key(
self, tmp_path: Path, key_file: Path, caplog: pytest.LogCaptureFixture
) -> None:
ledger = _ledger(
tmp_path,
[
_entry(
"credit",
submitted_at="soon",
key=key_file,
day=_NOW.date().isoformat(),
)
],
)
with caplog.at_level(logging.WARNING):
assert leetcode_solved_today(ledger, now=_NOW) is True
assert "unparsable submitted_at" in caplog.text
def test_missing_submitted_at_uses_day_key(
self, tmp_path: Path, key_file: Path
) -> None:
body: dict[str, Any] = {"entry_id": "x", "kind": "credit", "day": "2000-01-01"}
entry = {**body, "hmac": compute_entry_hmac(body, key_file=key_file)}
assert leetcode_solved_today(_ledger(tmp_path, [entry]), now=_NOW) is False
def test_resolves_module_path_when_none_given(
self, tmp_path: Path, key_file: Path
) -> None:
stamp = _NOW.timestamp()
ledger = _ledger(tmp_path, [_entry("credit", submitted_at=stamp, key=key_file)])
with patch.object(_leetcode_bonus, "LEETCODE_LEDGER_FILE", ledger):
assert leetcode_solved_today(now=_NOW) is True
class TestCannotCheck:
"""Every unreadable state is None -- never a confident False."""
def test_missing_ledger(self, tmp_path: Path, key_file: Path) -> None:
assert leetcode_solved_today(tmp_path / "missing.json") is None
def test_invalid_json(self, tmp_path: Path, key_file: Path) -> None:
path = tmp_path / "ledger.json"
path.write_text("{not json")
assert leetcode_solved_today(path) is None
def test_no_entries_array(self, tmp_path: Path, key_file: Path) -> None:
path = tmp_path / "ledger.json"
path.write_text(json.dumps({"entries": "nope"}))
assert leetcode_solved_today(path) is None
def test_unreadable_key(self, tmp_path: Path) -> None:
with patch.object(_leetcode_bonus, "HMAC_KEY_FILE", tmp_path / "no.key"):
assert leetcode_solved_today(_ledger(tmp_path, [])) is None
def test_empty_key(self, tmp_path: Path) -> None:
key = tmp_path / "hmac.key"
key.write_bytes(b" \n")
with patch.object(_leetcode_bonus, "HMAC_KEY_FILE", key):
assert leetcode_solved_today(_ledger(tmp_path, [])) is None
class TestBonusHours:
def test_solved_earns_the_hour(self) -> None:
with patch.object(_leetcode_bonus, "leetcode_solved_today", return_value=True):
assert leetcode_bonus_hours() == LEETCODE_BONUS_HOURS
def test_not_solved_earns_nothing(self) -> None:
with patch.object(_leetcode_bonus, "leetcode_solved_today", return_value=False):
assert leetcode_bonus_hours() == 0
def test_cannot_check_fails_closed_and_warns(
self, caplog: pytest.LogCaptureFixture
) -> None:
with (
patch.object(_leetcode_bonus, "leetcode_solved_today", return_value=None),
caplog.at_level(logging.WARNING),
):
assert leetcode_bonus_hours() == 0
assert "could not be checked" in caplog.text
class TestReadEntriesLabel:
"""_read_entries is shared with book-guard's reader; the label names whose."""
def test_default_label_is_leetcode(
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING):
assert _leetcode_bonus._read_entries(tmp_path / "missing.json") is None
assert "Cannot read the LeetCode ledger" in caplog.text
def test_custom_label_names_the_ledger(
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
path = tmp_path / "ledger.json"
path.write_text(json.dumps({"no": "entries"}))
with caplog.at_level(logging.WARNING):
assert _leetcode_bonus._read_entries(path, "book-guard") is None
assert "book-guard ledger at" in caplog.text
assert "LeetCode" not in caplog.text