This commit is contained in:
devRaGonSa
2026-06-05 16:57:25 +02:00
commit 0da8338ba8
310 changed files with 45849 additions and 0 deletions

View File

@@ -0,0 +1,497 @@
import gc
import json
import sqlite3
from datetime import datetime, timezone
from unittest.mock import patch
from app.rcon_admin_log_storage import (
initialize_rcon_admin_log_storage,
list_current_match_kill_feed,
list_rcon_admin_log_event_counts,
persist_rcon_admin_log_entries,
)
TARGET = {
"target_key": "test-rcon-target",
"external_server_id": "test-rcon-target",
}
def test_initialize_rcon_admin_log_storage_creates_event_table(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
resolved_path = initialize_rcon_admin_log_storage(db_path=db_path)
assert resolved_path == db_path
connection = sqlite3.connect(db_path)
try:
table_names = {
row[0]
for row in connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table'"
).fetchall()
}
columns = {
row[1]
for row in connection.execute("PRAGMA table_info(rcon_admin_log_events)")
}
finally:
connection.close()
gc.collect()
assert "rcon_admin_log_events" in table_names
assert "rcon_player_profile_snapshots" in table_names
assert {
"target_key",
"event_type",
"raw_message",
"canonical_message",
"parsed_payload_json",
"raw_entry_json",
}.issubset(columns)
def test_persist_rcon_admin_log_entries_inserts_then_reports_duplicates(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
entries = [
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] CONNECTED Player One (steam-1)",
},
{
"timestamp": "2026-05-19T10:01:00Z",
"message": "[2:00 min (120)] DISCONNECTED Player One (steam-1)",
},
]
first_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=entries,
db_path=db_path,
)
second_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=entries,
db_path=db_path,
)
assert first_delta == {
"events_seen": 2,
"events_inserted": 2,
"duplicate_events": 0,
}
assert second_delta == {
"events_seen": 2,
"events_inserted": 0,
"duplicate_events": 2,
}
gc.collect()
def test_profile_message_snapshots_are_materialized_and_deduped(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
entry = {
"timestamp": "2026-05-19T10:00:00Z",
"message": (
"[21:34:19 hours (1779108340)] MESSAGE: player [Jugador Uno(steam-profile-1)], "
"content [─ Jugador Uno ─\n"
"▒ Totales ▒\n"
"sesiones : 12\n"
"partidas jugadas : 9\n"
"bajas : 141 (6 TKs)\n"
"muertes : 268 (5 TKs)\n"
"K/D : 0.53\n"
"▒ Víctimas ▒\n"
"Rival Dos : 7\n"
"▒ Némesis ▒\n"
"Rival Tres : 4\n"
"▒ Armas favoritas ▒\n"
"M1 GARAND : 31\n"
"▒ Promedios ▒\n"
"bajas por partida : 15.6\n"
"▒ Sanciones ▒\n"
"kicks : 1]"
),
}
persist_rcon_admin_log_entries(target=TARGET, entries=[entry], db_path=db_path)
persist_rcon_admin_log_entries(target=TARGET, entries=[entry], db_path=db_path)
connection = sqlite3.connect(db_path)
connection.row_factory = sqlite3.Row
try:
rows = connection.execute("SELECT * FROM rcon_player_profile_snapshots").fetchall()
finally:
connection.close()
gc.collect()
assert len(rows) == 1
row = rows[0]
assert row["target_key"] == "test-rcon-target"
assert row["player_id"] == "steam-profile-1"
assert row["source_server_time"] == 1779108340
assert row["sessions"] == 12
assert row["matches_played"] == 9
assert row["total_kills"] == 141
assert row["total_deaths"] == 268
assert row["teamkills_done"] == 6
assert row["teamkills_received"] == 5
assert row["kd_ratio"] == 0.53
assert json.loads(row["favorite_weapons_json"]) == {"M1 GARAND": 31}
assert json.loads(row["victims_json"]) == {"Rival Dos": 7}
assert json.loads(row["nemesis_json"]) == {"Rival Tres": 4}
assert "bajas : 141" in row["raw_content"]
def test_non_profile_messages_do_not_create_profile_snapshots(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] MESSAGE: player [Player One(steam-1)], content [hello]",
}
],
db_path=db_path,
)
connection = sqlite3.connect(db_path)
try:
count = connection.execute(
"SELECT COUNT(*) FROM rcon_player_profile_snapshots"
).fetchone()[0]
finally:
connection.close()
gc.collect()
assert count == 0
def test_canonical_message_dedupes_changing_relative_prefixes(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
original_entry = {
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] MESSAGE: player [Player One(steam-1)], content [hello]",
}
repeated_read_entry = {
"timestamp": "2026-05-19T10:05:00Z",
"message": "[6:00 min (100)] MESSAGE: player [Player One(steam-1)], content [hello]",
}
first_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=[original_entry],
db_path=db_path,
)
second_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=[repeated_read_entry],
db_path=db_path,
)
assert first_delta["events_inserted"] == 1
assert second_delta == {
"events_seen": 1,
"events_inserted": 0,
"duplicate_events": 1,
}
gc.collect()
def test_list_rcon_admin_log_event_counts_groups_by_target_and_event_type(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
other_target = {
"target_key": "other-rcon-target",
"external_server_id": "other-rcon-target",
}
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] CONNECTED Player One (steam-1)",
},
{
"timestamp": "2026-05-19T10:01:00Z",
"message": "[2:00 min (120)] DISCONNECTED Player One (steam-1)",
},
],
db_path=db_path,
)
persist_rcon_admin_log_entries(
target=other_target,
entries=[
{
"timestamp": "2026-05-19T10:02:00Z",
"message": "[3:00 min (140)] CONNECTED Player Two (steam-2)",
}
],
db_path=db_path,
)
counts = {
(row["target_key"], row["event_type"]): row
for row in list_rcon_admin_log_event_counts(db_path=db_path)
}
assert counts == {
("other-rcon-target", "connected"): {
"target_key": "other-rcon-target",
"event_type": "connected",
"event_count": 1,
"first_server_time": 140,
"last_server_time": 140,
},
("test-rcon-target", "connected"): {
"target_key": "test-rcon-target",
"event_type": "connected",
"event_count": 1,
"first_server_time": 100,
"last_server_time": 100,
},
("test-rcon-target", "disconnected"): {
"target_key": "test-rcon-target",
"event_type": "disconnected",
"event_count": 1,
"first_server_time": 120,
"last_server_time": 120,
},
}
gc.collect()
def test_current_match_kill_feed_prefers_open_match_window_and_normalizes_rows(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-19T09:59:00Z",
"message": (
"[0:59 min (90)] KILL: Old Killer(Allies/steam-old) -> "
"Old Victim(Axis/steam-victim-old) with M1 GARAND"
),
},
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-19T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: Alpha(Allies/steam-alpha) -> "
"Bravo(Allies/steam-bravo) with GRENADE"
),
},
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
)
assert feed["scope"] == "open-admin-log-match-window"
assert feed["confidence"] == "admin-log-boundary"
assert len(feed["items"]) == 1
assert feed["items"][0] == {
"event_id": "rcon-admin-log:test-rcon-target:3",
"event_timestamp": "2026-05-19T10:01:00Z",
"server_time": 120,
"killer_name": "Alpha",
"killer_team": "Allies",
"victim_name": "Bravo",
"victim_team": "Allies",
"weapon": "GRENADE",
"is_teamkill": True,
}
gc.collect()
def test_current_match_kill_feed_filters_stale_recent_fallback_rows(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T09:30:00Z",
"message": (
"[1:00 min (1779355800)] KILL: Old Killer(Allies/steam-old) -> "
"Old Victim(Axis/steam-victim-old) with M1 GARAND"
),
}
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
now=datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc),
)
assert feed["scope"] == "no-current-match-events"
assert feed["confidence"] == "stale-filtered"
assert feed["stale_events_filtered"] == 1
assert feed["items"] == []
gc.collect()
def test_current_match_kill_feed_marks_fresh_recent_fallback_rows_partial(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T09:50:00Z",
"message": (
"[1:00 min (1779357000)] KILL: Fresh Killer(Allies/steam-fresh) -> "
"Fresh Victim(Axis/steam-victim-fresh) with M1 GARAND"
),
}
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
now=datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc),
)
assert feed["scope"] == "recent-admin-log-window"
assert feed["confidence"] == "partial"
assert feed["stale_events_filtered"] == 0
assert [item["killer_name"] for item in feed["items"]] == ["Fresh Killer"]
gc.collect()
def test_current_match_kill_feed_filters_rows_before_incremental_cursor(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: First Killer(Allies/steam-first) -> "
"First Victim(Axis/steam-first-victim) with M1 GARAND"
),
},
{
"timestamp": "2026-05-21T10:02:00Z",
"message": (
"[3:00 min (140)] KILL: Next Killer(Axis/steam-next) -> "
"Next Victim(Allies/steam-next-victim) with MP40"
),
},
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
since_event_id="rcon-admin-log:test-rcon-target:2",
)
assert [item["killer_name"] for item in feed["items"]] == ["Next Killer"]
gc.collect()
def test_current_match_kill_feed_without_cursor_omits_nullable_id_predicate(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: Cursor Killer(Allies/steam-cursor) -> "
"Cursor Victim(Axis/steam-cursor-victim) with M1 GARAND"
),
},
],
db_path=db_path,
)
traced_sql = []
connect = sqlite3.connect
def connect_with_trace(*args, **kwargs):
connection = connect(*args, **kwargs)
connection.set_trace_callback(traced_sql.append)
return connection
with patch(
"app.rcon_admin_log_storage.sqlite3.connect",
side_effect=connect_with_trace,
):
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
)
kill_queries = [
sql
for sql in traced_sql
if "FROM rcon_admin_log_events" in sql and "event_type = 'kill'" in sql
]
assert [item["killer_name"] for item in feed["items"]] == ["Cursor Killer"]
assert kill_queries
assert all("IS NULL OR id >" not in sql for sql in kill_queries)
assert all("AND id >" not in sql for sql in kill_queries)
gc.collect()
def test_current_match_kill_feed_invalid_cursor_behaves_like_no_cursor(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: First Killer(Allies/steam-first) -> "
"First Victim(Axis/steam-first-victim) with M1 GARAND"
),
},
{
"timestamp": "2026-05-21T10:02:00Z",
"message": (
"[3:00 min (140)] KILL: Next Killer(Axis/steam-next) -> "
"Next Victim(Allies/steam-next-victim) with MP40"
),
},
],
db_path=db_path,
)
without_cursor = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
)
with_invalid_cursor = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
since_event_id="not-an-admin-log-event",
)
assert with_invalid_cursor == without_cursor
gc.collect()