Add RCON AdminLog storage tests

This commit is contained in:
devRaGonSa
2026-05-19 14:27:35 +02:00
parent 9f14ce0220
commit 2a1c23b4fd
2 changed files with 193 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
---
id: TASK-121
title: Add RCON AdminLog storage read tests
status: pending
status: done
type: backend
team: Backend Senior
supporting_teams:
@@ -72,3 +72,17 @@ AdminLog storage and manual ingestion exist, and real validation confirmed canon
- Stage only intended files.
- Commit the completed implementation.
- Push the branch to origin.
## Outcome
- Added deterministic offline storage tests in `backend/tests/test_rcon_admin_log_storage.py`.
- Covered AdminLog table initialization, first insert behavior, duplicate reporting, canonical-message dedupe when relative prefixes change, and event-count grouping.
- Tests use pytest `tmp_path` temporary SQLite paths and do not use real RCON or the runtime database.
## Validation Result
- `python -m compileall backend/app` passed.
- `python -m pytest backend/tests/test_rcon_admin_log_parser.py backend/tests/test_rcon_admin_log_storage.py` could not run locally because `pytest` is not installed.
- `docker compose exec backend python -m pytest backend/tests/test_rcon_admin_log_parser.py backend/tests/test_rcon_admin_log_storage.py` also could not run because `pytest` is not installed in the backend image.
- Direct Python invocation of the new storage test functions passed with `PYTHONPATH=backend`.
- `git diff --name-only` matched the expected task scope after accounting for the new untracked test file.

View File

@@ -0,0 +1,178 @@
import gc
import sqlite3
from app.rcon_admin_log_storage import (
initialize_rcon_admin_log_storage,
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 {
"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_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()