Stabilize player stats and historical detail public reads
This commit is contained in:
@@ -7,9 +7,16 @@ import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.historical_storage import upsert_historical_match
|
||||
from app.payloads import build_recent_historical_matches_payload
|
||||
from app.rcon_historical_player_stats import (
|
||||
get_rcon_materialized_player_stats,
|
||||
initialize_player_period_stats_storage,
|
||||
initialize_player_search_index_storage,
|
||||
search_rcon_materialized_players,
|
||||
)
|
||||
from app.rcon_admin_log_materialization import (
|
||||
get_materialized_rcon_match_detail,
|
||||
materialize_rcon_admin_log,
|
||||
@@ -298,6 +305,85 @@ class RconMaterializationPipelineTests(unittest.TestCase):
|
||||
self.assertEqual(payload["data"]["items"][0]["result_source"], "public-scoreboard-fallback")
|
||||
gc.collect()
|
||||
|
||||
def test_public_player_search_uses_read_model_without_initialize_or_runtime_fallback(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "historical.sqlite3"
|
||||
initialize_player_search_index_storage(db_path=db_path)
|
||||
_insert_player_search_index_fixture(db_path)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"app.rcon_historical_player_stats.initialize_player_search_index_storage",
|
||||
side_effect=AssertionError("public read must not initialize player search storage"),
|
||||
),
|
||||
patch(
|
||||
"app.rcon_historical_player_stats._search_rcon_materialized_players_runtime",
|
||||
side_effect=AssertionError("public read must not use runtime player search fallback"),
|
||||
),
|
||||
):
|
||||
payload = search_rcon_materialized_players(
|
||||
query="Medu",
|
||||
server_id="all",
|
||||
limit=10,
|
||||
db_path=db_path,
|
||||
)
|
||||
|
||||
self.assertEqual(payload["source"]["read_model"], "player-search-index")
|
||||
self.assertFalse(payload["source"]["fallback_used"])
|
||||
self.assertEqual(payload["items"][0]["player_id"], "76561198092154180")
|
||||
gc.collect()
|
||||
|
||||
def test_public_player_detail_returns_controlled_empty_without_initialize_or_runtime_fallback(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "missing.sqlite3"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"app.rcon_historical_player_stats.initialize_player_period_stats_storage",
|
||||
side_effect=AssertionError("public read must not initialize player period storage"),
|
||||
),
|
||||
patch(
|
||||
"app.rcon_historical_player_stats._get_rcon_materialized_player_stats_runtime",
|
||||
side_effect=AssertionError("public read must not use runtime player detail fallback"),
|
||||
),
|
||||
):
|
||||
payload = get_rcon_materialized_player_stats(
|
||||
player_id="76561198092154180",
|
||||
server_id="all",
|
||||
timeframe="weekly",
|
||||
db_path=db_path,
|
||||
)
|
||||
|
||||
self.assertEqual(payload["player_id"], "76561198092154180")
|
||||
self.assertEqual(payload["matches_considered"], 0)
|
||||
self.assertEqual(payload["source"]["read_model"], "player-period-stats")
|
||||
self.assertEqual(payload["source"]["status"], "unavailable")
|
||||
self.assertFalse(payload["source"]["fallback_used"])
|
||||
gc.collect()
|
||||
|
||||
def test_public_match_detail_read_does_not_initialize_materialized_storage(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "historical.sqlite3"
|
||||
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
|
||||
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
|
||||
try:
|
||||
_persist_admin_log_fixture(db_path)
|
||||
materialize_rcon_admin_log(db_path=db_path)
|
||||
with patch(
|
||||
"app.rcon_admin_log_materialization.initialize_rcon_materialized_storage",
|
||||
side_effect=AssertionError("public match detail read must not initialize storage"),
|
||||
):
|
||||
detail = get_rcon_historical_match_detail(
|
||||
server_key="comunidad-hispana-01",
|
||||
match_id="comunidad-hispana-01:100:500:stmariedumontwarfare",
|
||||
)
|
||||
finally:
|
||||
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
|
||||
|
||||
self.assertIsNotNone(detail)
|
||||
self.assertEqual(detail["result_source"], "admin-log-match-ended")
|
||||
gc.collect()
|
||||
|
||||
def test_safe_scoreboard_match_url_allowlist_for_active_origins(self) -> None:
|
||||
self.assertEqual(
|
||||
resolve_trusted_scoreboard_match_url(
|
||||
@@ -385,6 +471,44 @@ def _persist_scoreboard_match(db_path: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _insert_player_search_index_fixture(db_path: Path) -> None:
|
||||
import sqlite3
|
||||
|
||||
with sqlite3.connect(db_path) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO player_search_index (
|
||||
server_id,
|
||||
player_id,
|
||||
player_name,
|
||||
normalized_player_name,
|
||||
first_seen_at,
|
||||
last_seen_at,
|
||||
servers_seen,
|
||||
matches_current_year,
|
||||
kills_current_year,
|
||||
deaths_current_year,
|
||||
teamkills_current_year,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"all-servers",
|
||||
"76561198092154180",
|
||||
"Medu",
|
||||
"medu",
|
||||
"2026-01-01T00:00:00Z",
|
||||
"2026-06-01T00:00:00Z",
|
||||
'["comunidad-hispana-01"]',
|
||||
12,
|
||||
100,
|
||||
50,
|
||||
0,
|
||||
"2026-06-01T00:00:00Z",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _restore_env(name: str, previous_value: str | None) -> None:
|
||||
if previous_value is None:
|
||||
os.environ.pop(name, None)
|
||||
|
||||
Reference in New Issue
Block a user