mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 18:13:49 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / web-coverage (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
223 lines
7.4 KiB
Python
223 lines
7.4 KiB
Python
"""Tests for _leetcode_ledger: which entries count as a solve today.
|
|
|
|
The signature check is load-bearing. The shared reader in ``earned_time``
|
|
reproduces gatelock's canonicalisation rather than importing it (gatelock pulls
|
|
in tkinter, and the enforcer runs headless as root).
|
|
``TestSignatureCanonicalisation`` pins it to a fixed vector so the duplication
|
|
cannot drift silently -- if leetcode-guard ever changes how it signs, that test
|
|
fails instead of every credit quietly ceasing to count.
|
|
|
|
Every read here passes ``gate_off=True``: these pin how a credit row is
|
|
parsed, which only decides on a day the gate stands down. On any other day
|
|
only the paid-in-full charge earns (``test_leetcode_paid_day.py``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import TYPE_CHECKING
|
|
|
|
import earned_time
|
|
|
|
from steam_backlog_enforcer._leetcode_ledger import read_ledger_solved_today
|
|
from steam_backlog_enforcer.tests._ledger_fixtures import (
|
|
KEY,
|
|
credit,
|
|
key_file,
|
|
sign,
|
|
write_ledger,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
__all__ = ["key_file"]
|
|
|
|
|
|
class TestSignatureCanonicalisation:
|
|
"""The shared canonicalisation must keep matching gatelock's."""
|
|
|
|
def test_a_known_vector_still_verifies(self) -> None:
|
|
"""Pinned so a change in how entries are signed fails loudly here."""
|
|
entry = {
|
|
"entry_id": "ac:1",
|
|
"kind": "credit",
|
|
"day": "2026-08-30",
|
|
"created_at": "2026-08-30T14:00:00+02:00",
|
|
"amount": 1,
|
|
"device": "pc",
|
|
"detail": {
|
|
"title_slug": "two-sum",
|
|
"lang": "python3",
|
|
"source": "leetcode",
|
|
"submitted_at": "1788090000",
|
|
},
|
|
"hmac": "4f6986bdb019bf28f607918ffb1b5bc79d252f887ade2e25aa83a31d1e49423e",
|
|
}
|
|
assert earned_time.entry_signature(entry, KEY) == entry["hmac"]
|
|
|
|
def test_a_tampered_entry_fails(self, tmp_path: Path, key_file: Path) -> None:
|
|
"""Changing any field must invalidate the signature.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
"""
|
|
assert key_file.exists()
|
|
entry = credit(when=datetime.now().astimezone())
|
|
entry["amount"] = 99
|
|
assert (
|
|
read_ledger_solved_today(write_ledger(tmp_path, [entry]), gate_off=True)
|
|
is False
|
|
)
|
|
|
|
def test_a_missing_signature_fails(self, tmp_path: Path, key_file: Path) -> None:
|
|
"""An entry with no hmac field is not a signed entry.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
"""
|
|
assert key_file.exists()
|
|
entry = credit(when=datetime.now().astimezone())
|
|
del entry["hmac"]
|
|
assert (
|
|
read_ledger_solved_today(write_ledger(tmp_path, [entry]), gate_off=True)
|
|
is False
|
|
)
|
|
|
|
|
|
class TestReadingTheLedger:
|
|
"""Which entries count, and which are ignored."""
|
|
|
|
def test_a_credit_today_counts(self, tmp_path: Path, key_file: Path) -> None:
|
|
"""The ordinary case.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
"""
|
|
assert key_file.exists()
|
|
ledger = write_ledger(tmp_path, [credit(when=datetime.now().astimezone())])
|
|
assert read_ledger_solved_today(ledger, gate_off=True) is True
|
|
|
|
def test_a_credit_yesterday_does_not(self, tmp_path: Path, key_file: Path) -> None:
|
|
"""Yesterday's solve bought yesterday's hour, not today's.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
"""
|
|
assert key_file.exists()
|
|
yesterday = datetime.now().astimezone() - timedelta(days=1)
|
|
ledger = write_ledger(tmp_path, [credit(when=yesterday)])
|
|
assert read_ledger_solved_today(ledger, gate_off=True) is False
|
|
|
|
def test_charges_and_seen_entries_do_not_count(
|
|
self, tmp_path: Path, key_file: Path
|
|
) -> None:
|
|
"""A settled day is not a solved day, and seeding is worth nothing.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
"""
|
|
assert key_file.exists()
|
|
now = datetime.now().astimezone()
|
|
today = now.date().isoformat()
|
|
rows = [
|
|
sign(
|
|
{
|
|
"entry_id": f"charge:{today}",
|
|
"kind": "charge",
|
|
"day": today,
|
|
"created_at": now.isoformat(),
|
|
"amount": 2,
|
|
"device": "pc",
|
|
"detail": {"source": "escape"},
|
|
}
|
|
),
|
|
sign(
|
|
{
|
|
"entry_id": "ac:9",
|
|
"kind": "seen",
|
|
"day": today,
|
|
"created_at": now.isoformat(),
|
|
"amount": 0,
|
|
"device": "pc",
|
|
"detail": {"submitted_at": str(int(now.timestamp()))},
|
|
}
|
|
),
|
|
]
|
|
assert (
|
|
read_ledger_solved_today(write_ledger(tmp_path, rows), gate_off=True)
|
|
is False
|
|
)
|
|
|
|
def test_a_forged_credit_does_not_count(
|
|
self, tmp_path: Path, key_file: Path
|
|
) -> None:
|
|
"""Appending JSON must not buy an hour.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
"""
|
|
assert key_file.exists()
|
|
forged = credit(when=datetime.now().astimezone())
|
|
forged["hmac"] = "00" * 32
|
|
assert (
|
|
read_ledger_solved_today(write_ledger(tmp_path, [forged]), gate_off=True)
|
|
is False
|
|
)
|
|
|
|
def test_a_missing_submitted_at_falls_back_to_the_day_key(
|
|
self, tmp_path: Path, key_file: Path
|
|
) -> None:
|
|
"""Older credits predate the field and must still count.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
"""
|
|
assert key_file.exists()
|
|
now = datetime.now().astimezone()
|
|
entry = sign(
|
|
{
|
|
"entry_id": "ac:2",
|
|
"kind": "credit",
|
|
"day": now.date().isoformat(),
|
|
"created_at": now.isoformat(),
|
|
"amount": 1,
|
|
"device": "pc",
|
|
"detail": {"source": "leetcode"},
|
|
}
|
|
)
|
|
assert (
|
|
read_ledger_solved_today(write_ledger(tmp_path, [entry]), gate_off=True)
|
|
is True
|
|
)
|
|
|
|
def test_an_unparsable_submitted_at_is_reported(
|
|
self, tmp_path: Path, key_file: Path, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Corrupt data falls back rather than crashing, and says so.
|
|
|
|
Args:
|
|
tmp_path: pytest's temporary directory.
|
|
key_file: The patched signing key.
|
|
caplog: pytest's log capture.
|
|
"""
|
|
assert key_file.exists()
|
|
now = datetime.now().astimezone()
|
|
entry = credit(when=now, entry_id="ac:3")
|
|
entry["detail"]["submitted_at"] = "not-a-number"
|
|
entry = sign({k: v for k, v in entry.items() if k != "hmac"})
|
|
assert (
|
|
read_ledger_solved_today(write_ledger(tmp_path, [entry]), gate_off=True)
|
|
is True
|
|
)
|
|
assert "unparsable submitted_at" in caplog.text
|