from __future__ import annotations import os import unittest from contextlib import contextmanager from types import SimpleNamespace from unittest.mock import patch from app.rcon_historical_worker import ( CAPTURE_MODE_CURRENT_LIVE, CAPTURE_MODE_HISTORICAL, build_arg_parser, main, run_rcon_historical_capture, run_rcon_historical_capture_unlocked, ) TARGET = SimpleNamespace( external_server_id="comunidad-hispana-01", name="Comunidad Hispana #01", host="203.0.113.10", port=7779, region="ES", game_port=7777, query_port=7778, source_name="community-hispana-rcon", ) class RconHistoricalWorkerTests(unittest.TestCase): def test_current_live_capture_skips_materialization(self) -> None: with ( patch("app.rcon_historical_worker.initialize_rcon_historical_storage"), patch("app.rcon_historical_worker._select_targets", return_value=[TARGET]), patch( "app.rcon_historical_worker.query_live_server_sample", return_value={"normalized": {"players": 10}, "raw_session": {"raw": True}}, ), patch( "app.rcon_historical_worker.persist_rcon_historical_sample", return_value={"samples_inserted": 1, "duplicate_samples": 0}, ), patch( "app.rcon_historical_worker._ingest_target_admin_log", return_value={ "status": "ok", "errors": [], "totals": { "events_seen": 4, "events_inserted": 2, "duplicate_events": 2, "failed_targets": 0, }, }, ), patch("app.rcon_historical_worker.start_rcon_historical_capture_run", return_value=101), patch("app.rcon_historical_worker.finalize_rcon_historical_capture_run"), patch( "app.rcon_historical_worker.list_rcon_historical_target_statuses", return_value=[{"target_key": "comunidad-hispana-01"}], ), patch("app.rcon_historical_worker.materialize_rcon_admin_log") as materialize, ): payload = run_rcon_historical_capture_unlocked(capture_mode=CAPTURE_MODE_CURRENT_LIVE) materialize.assert_not_called() self.assertEqual(payload["capture_mode"], CAPTURE_MODE_CURRENT_LIVE) self.assertIs(payload["materialization_skipped"], True) self.assertEqual(payload["admin_log_events_seen"], 4) self.assertEqual(payload["admin_log_events_inserted"], 2) self.assertEqual(payload["duplicate_events"], 2) self.assertEqual(payload["samples_inserted"], 1) self.assertEqual(payload["materialization_result"]["status"], "skipped") def test_historical_capture_keeps_materialization(self) -> None: with ( patch("app.rcon_historical_worker.initialize_rcon_historical_storage"), patch("app.rcon_historical_worker._select_targets", return_value=[TARGET]), patch( "app.rcon_historical_worker.query_live_server_sample", return_value={"normalized": {"players": 10}, "raw_session": {"raw": True}}, ), patch( "app.rcon_historical_worker.persist_rcon_historical_sample", return_value={"samples_inserted": 1, "duplicate_samples": 0}, ), patch( "app.rcon_historical_worker._ingest_target_admin_log", return_value={ "status": "ok", "errors": [], "totals": { "events_seen": 1, "events_inserted": 1, "duplicate_events": 0, "failed_targets": 0, }, }, ), patch("app.rcon_historical_worker.start_rcon_historical_capture_run", return_value=102), patch("app.rcon_historical_worker.finalize_rcon_historical_capture_run"), patch( "app.rcon_historical_worker.list_rcon_historical_target_statuses", return_value=[{"target_key": "comunidad-hispana-01"}], ), patch( "app.rcon_historical_worker.materialize_rcon_admin_log", return_value={"matches_materialized": 3, "matches_updated": 2}, ) as materialize, ): payload = run_rcon_historical_capture_unlocked(capture_mode=CAPTURE_MODE_HISTORICAL) materialize.assert_called_once_with() self.assertEqual(payload["capture_mode"], CAPTURE_MODE_HISTORICAL) self.assertIs(payload["materialization_skipped"], False) self.assertEqual(payload["totals"]["materialized_matches_inserted"], 3) self.assertEqual(payload["totals"]["materialized_matches_updated"], 2) def test_cli_and_env_can_activate_current_live_mode(self) -> None: with _temporary_env( HLL_RCON_CURRENT_MATCH_MODE="true", HLL_RCON_CURRENT_MATCH_CAPTURE_INTERVAL_SECONDS="5", ): args = build_arg_parser().parse_args(["loop"]) with patch("app.rcon_historical_worker.run_periodic_rcon_historical_capture") as runner: exit_code = main(["loop"]) self.assertEqual(args.capture_mode, CAPTURE_MODE_CURRENT_LIVE) self.assertEqual(exit_code, 0) runner.assert_called_once() self.assertEqual(runner.call_args.kwargs["capture_mode"], CAPTURE_MODE_CURRENT_LIVE) self.assertEqual(runner.call_args.kwargs["interval_seconds"], 5) explicit = build_arg_parser().parse_args(["capture", "--skip-materialization"]) self.assertIs(explicit.skip_materialization, True) def test_current_live_capture_uses_short_lock_timeout(self) -> None: seen: dict[str, object] = {} @contextmanager def fake_lock(**kwargs): seen.update(kwargs) yield {"holder": kwargs["holder"]} with ( _temporary_env(HLL_RCON_CURRENT_MATCH_WRITER_LOCK_TIMEOUT_SECONDS="3.5"), patch("app.rcon_historical_worker.backend_writer_lock", side_effect=fake_lock), patch( "app.rcon_historical_worker.run_rcon_historical_capture_unlocked", return_value={"status": "ok"}, ), ): run_rcon_historical_capture(capture_mode=CAPTURE_MODE_CURRENT_LIVE) self.assertEqual(seen["timeout_seconds"], 3.5) @contextmanager def _temporary_env(**values: str): previous = {name: os.environ.get(name) for name in values} try: for name, value in values.items(): os.environ[name] = value yield finally: for name, value in previous.items(): if value is None: os.environ.pop(name, None) else: os.environ[name] = value