A02/A03: Add state validation and normalize subdomain keys for shared cooldown

This commit is contained in:
ik
2026-09-10 20:34:05 +07:00
parent f29ec706fd
commit 57aa3ffafb
2 changed files with 94 additions and 10 deletions
+56 -5
View File
@@ -28,6 +28,19 @@ def test_fetch_site_key_groups_endpoints_and_normalizes_www() -> None:
assert fetch_site_key("https://www.rf4-stat.ru/posts/") == "rf4-stat.ru"
def test_fetch_site_key_normalizes_common_subdomains() -> None:
"""A02: download/api/cdn subdomains share the base domain key."""
assert fetch_site_key("https://download.rf4db.com/ru/catches") == "rf4db.com"
assert fetch_site_key("https://api.rf4db.com/v1/catches") == "rf4db.com"
assert fetch_site_key("https://cdn.rf4db.com/assets/img.jpg") == "rf4db.com"
assert fetch_site_key("https://www.rf4db.com/") == "rf4db.com"
# Base domain stays the same
assert fetch_site_key("https://rf4db.com/") == "rf4db.com"
# Non-normalized subdomains stay as-is
assert fetch_site_key("https://rf4map.ru/point/1") == "rf4map.ru"
assert fetch_site_key("https://rf4-posts.com/spots/") == "rf4-posts.com"
def test_failed_fetch_still_reserves_site_cooldown(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
state_file = tmp_path / "fetch-state.json"
@@ -36,7 +49,8 @@ def test_failed_fetch_still_reserves_site_cooldown(tmp_path: Path, monkeypatch:
monkeypatch.setattr(community_cli, "fetch_html", fail)
assert community_cli.main(["rf4db", "--state-file", str(state_file)]) == 1
assert "download.rf4db.com" in json.loads(state_file.read_text(encoding="utf-8"))
# A02: download.rf4db.com normalized to rf4db.com
assert "rf4db.com" in json.loads(state_file.read_text(encoding="utf-8"))
def test_validate_url_host_rejects_disallowed_hosts() -> None:
@@ -73,15 +87,15 @@ def test_write_state_is_atomic_with_flush(tmp_path: Path) -> None:
from rf4_research.community_cli import _read_state, _write_state
state_file = tmp_path / "state.json"
_write_state(state_file, {"key1": "value1"})
_write_state(state_file, {"key1": 1000.0})
assert state_file.exists()
assert not (state_file.with_suffix(".tmp")).exists()
state = _read_state(state_file)
assert state == {"key1": "value1"}
assert state == {"key1": 1000.0}
_write_state(state_file, {"key1": "value2", "key2": "value3"})
_write_state(state_file, {"key1": 2000.0, "key2": 3000.0})
state = _read_state(state_file)
assert state == {"key1": "value2", "key2": "value3"}
assert state == {"key1": 2000.0, "key2": 3000.0}
assert not (state_file.with_suffix(".tmp")).exists()
@@ -273,3 +287,40 @@ def test_urljoin_resolves_relative_redirects() -> None:
assert urljoin("https://rf4-stat.ru/old/path", "new") == "https://rf4-stat.ru/old/new"
# Absolute URL
assert urljoin("https://rf4-stat.ru/old", "https://rf4-stat.ru/absolute") == "https://rf4-stat.ru/absolute"
# A02: State validation tests
def test_read_state_rejects_corrupt_json(tmp_path: Path) -> None:
"""A02: Corrupt JSON returns empty dict."""
from rf4_research.community_cli import _read_state
state_file = tmp_path / "corrupt.json"
state_file.write_text("not valid json {{{")
assert _read_state(state_file) == {}
def test_read_state_rejects_invalid_structure(tmp_path: Path) -> None:
"""A02: Non-dict or non-flat structures return empty dict."""
from rf4_research.community_cli import _read_state
state_file = tmp_path / "invalid.json"
# List instead of dict
state_file.write_text("[1, 2, 3]")
assert _read_state(state_file) == {}
# Dict with non-number values
state_file.write_text('{"key": "value"}')
assert _read_state(state_file) == {}
# Dict with nested dict
state_file.write_text('{"key": {"nested": true}}')
assert _read_state(state_file) == {}
# Dict with non-string keys (JSON always has string keys, but validate anyway)
state_file.write_text('{"valid": 123}')
assert _read_state(state_file) == {"valid": 123}
def test_read_state_handles_missing_file() -> None:
"""A02: Missing state file returns empty dict."""
from rf4_research.community_cli import _read_state
assert _read_state(Path("/nonexistent/state.json")) == {}