mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 21:27:13 +01:00
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
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
196 lines
7.0 KiB
Python
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
|