fix: tautulli — hmac token compare, public pattern loader, startup cache, endpoint tests
This commit is contained in:
parent
581e0314b4
commit
279b01902f
3 changed files with 75 additions and 3 deletions
|
|
@ -42,6 +42,11 @@ def _compile(patterns: list[LogPattern]) -> list[tuple[LogPattern, re.Pattern]]:
|
|||
return [(p, re.compile(p.pattern, re.IGNORECASE)) for p in patterns]
|
||||
|
||||
|
||||
def load_compiled_patterns(path: Path) -> list[tuple[LogPattern, object]]:
|
||||
"""Load and compile patterns from a YAML file. Public API over the private _compile."""
|
||||
return _compile(load_patterns(path))
|
||||
|
||||
|
||||
def apply_patterns(
|
||||
text: str,
|
||||
compiled: list[tuple[LogPattern, re.Pattern]],
|
||||
|
|
|
|||
12
app/rest.py
12
app/rest.py
|
|
@ -8,6 +8,7 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
|
|
@ -24,7 +25,7 @@ from fastapi.staticfiles import StaticFiles
|
|||
from pydantic import BaseModel
|
||||
|
||||
from app.ingest.pipeline import ensure_schema
|
||||
from app.ingest.base import _compile, load_patterns
|
||||
from app.ingest.base import load_compiled_patterns, load_patterns
|
||||
from app.ingest.tautulli import parse_webhook as _parse_tautulli
|
||||
from app.services.incidents import (
|
||||
build_bundle,
|
||||
|
|
@ -68,11 +69,14 @@ PATTERN_DIR = Path(os.environ.get("TURNSTONE_PATTERNS", Path(__file__).parent.pa
|
|||
PATTERN_FILE = PATTERN_DIR / "default.yaml"
|
||||
|
||||
_watcher = Watcher(DB_PATH, PATTERN_FILE)
|
||||
_compiled_patterns: list = []
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _lifespan(app: FastAPI):
|
||||
global _compiled_patterns
|
||||
ensure_schema(DB_PATH)
|
||||
_compiled_patterns = load_compiled_patterns(PATTERN_FILE)
|
||||
watch_cfg_path = PATTERN_DIR / "watch.yaml"
|
||||
configs = load_watch_config(watch_cfg_path)
|
||||
if configs:
|
||||
|
|
@ -360,7 +364,9 @@ def watch_status() -> dict:
|
|||
@router.post("/api/watch/reload")
|
||||
def watch_reload() -> dict:
|
||||
"""Stop all watch sources and restart with current watch.yaml."""
|
||||
global _compiled_patterns
|
||||
_watcher.stop()
|
||||
_compiled_patterns = load_compiled_patterns(PATTERN_FILE)
|
||||
watch_cfg_path = PATTERN_DIR / "watch.yaml"
|
||||
configs = load_watch_config(watch_cfg_path)
|
||||
if configs:
|
||||
|
|
@ -496,13 +502,13 @@ def ingest_tautulli(
|
|||
token = prefs.get("tautulli_token", "")
|
||||
if token:
|
||||
header_token = request.headers.get("X-Tautulli-Token", "")
|
||||
if header_token != token:
|
||||
if not hmac.compare_digest(header_token, token):
|
||||
raise HTTPException(status_code=403, detail="Invalid Tautulli token")
|
||||
|
||||
if "action" not in payload:
|
||||
raise HTTPException(status_code=400, detail="Missing required field: action")
|
||||
|
||||
compiled = _compile(load_patterns(PATTERN_FILE))
|
||||
compiled = _compiled_patterns
|
||||
entry = _parse_tautulli(payload, compiled)
|
||||
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.ingest.tautulli import is_tautulli_payload, parse_webhook
|
||||
|
||||
|
|
@ -242,3 +243,63 @@ class TestFixedFields:
|
|||
def test_entry_id_is_nonempty_string(self):
|
||||
entry = parse_webhook(_ERROR_PAYLOAD, [])
|
||||
assert isinstance(entry.entry_id, str) and len(entry.entry_id) > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoint tests — auth, validation, happy path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEndpoint:
|
||||
@pytest.fixture
|
||||
def client(self, tmp_path):
|
||||
from fastapi.testclient import TestClient
|
||||
from app.ingest.pipeline import ensure_schema
|
||||
import app.rest as rest_module
|
||||
|
||||
db = tmp_path / "test.db"
|
||||
ensure_schema(db)
|
||||
|
||||
with patch.object(rest_module, "DB_PATH", db), \
|
||||
patch.object(rest_module, "PREFS_PATH", tmp_path / "prefs.json"), \
|
||||
patch.object(rest_module, "_compiled_patterns", []):
|
||||
with TestClient(rest_module.app, raise_server_exceptions=True) as c:
|
||||
yield c
|
||||
|
||||
def test_missing_action_returns_400(self, client):
|
||||
resp = client.post(
|
||||
"/turnstone/api/ingest/tautulli",
|
||||
json={"session_key": "x"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_wrong_token_returns_403(self, tmp_path):
|
||||
from fastapi.testclient import TestClient
|
||||
from app.ingest.pipeline import ensure_schema
|
||||
import app.rest as rest_module
|
||||
|
||||
db = tmp_path / "test.db"
|
||||
ensure_schema(db)
|
||||
prefs_path = tmp_path / "prefs.json"
|
||||
import json as _json
|
||||
prefs_path.write_text(_json.dumps({"tautulli_token": "secret"}))
|
||||
|
||||
with patch.object(rest_module, "DB_PATH", db), \
|
||||
patch.object(rest_module, "PREFS_PATH", prefs_path), \
|
||||
patch.object(rest_module, "_compiled_patterns", []):
|
||||
with TestClient(rest_module.app, raise_server_exceptions=True) as c:
|
||||
resp = c.post(
|
||||
"/turnstone/api/ingest/tautulli",
|
||||
json=_ERROR_PAYLOAD,
|
||||
headers={"X-Tautulli-Token": "wrong"},
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_valid_payload_returns_200(self, client):
|
||||
resp = client.post(
|
||||
"/turnstone/api/ingest/tautulli",
|
||||
json=_ERROR_PAYLOAD,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["stored"] == 1
|
||||
assert "entry_id" in data
|
||||
|
|
|
|||
Loading…
Reference in a new issue