fix(e5): deep-review survivors — TMDB key log leak, person-enrich rollback + timeout
Three verified deep-review findings (the 4th, the filter combobox a11y gap, is deferred to a dedicated session on the combobox architecture): - SEC (medium): TmdbClient sent the api_key as a URL query param, so an HTTP error's httpx exception string embedded the full URL incl. the key, which enrich_batch logged. _get now raises status+path only (never str(e)/the URL) and severs the secret-bearing cause with `from None`. Regression test asserts neither the status nor the network error path contains the key. - STATE (low): person_detail's lazy-enrich except now db.rollback()s, so a failed enrich_person commit can't poison the session and 500 the subsequent SELECTs (SQLAlchemy 2.x deactivates the tx on a commit-time flush failure and does not auto-reset it). Test locks that the page renders despite a poisoned enrich. - ABUSE (low): the request-path lazy person enrich now caps its TMDB client at an 8s timeout (was the 30s default) so a slow TMDB can't tie up a worker; TmdbClient gained a `timeout` param (batch keeps 30s). The fuller fix (move person enrichment off the request path) is logged for later.
This commit is contained in:
@@ -5,6 +5,9 @@ recommendations to library rows; the whole thing no-ops without an API key).
|
||||
The DB tests use the `db` fixture (real Postgres) — skipped when none is reachable."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.models import PlexItem, PlexLibrary, PlexPerson, PlexShow
|
||||
from app.plex import tmdb
|
||||
|
||||
@@ -198,6 +201,44 @@ def test_serve_block_none_until_enriched_and_resolves_recs(db):
|
||||
assert recs[999]["in_library"] is False and recs[999]["rating_key"] is None
|
||||
|
||||
|
||||
def test_get_never_leaks_api_key_in_error_messages(monkeypatch):
|
||||
# The api_key rides in the request URL query string, so an error message built from the httpx
|
||||
# exception (which embeds that URL) would leak the secret to logs (enrich_batch logs it). Both the
|
||||
# HTTP-status and the network error paths must report status/path only — never the key.
|
||||
monkeypatch.setattr(
|
||||
tmdb.sysconfig, "get_str", lambda d, k: "SECRETKEY" if k == "tmdb_api_key" else "en-US"
|
||||
)
|
||||
client = tmdb.TmdbClient(None)
|
||||
req = httpx.Request(
|
||||
"GET", "https://api.themoviedb.org/3/movie/1?api_key=SECRETKEY&language=en-US"
|
||||
)
|
||||
|
||||
class _StatusHttp:
|
||||
def get(self, url, params=None):
|
||||
return httpx.Response(401, request=req) # non-404 → raise_for_status → HTTPStatusError
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
client._http = _StatusHttp()
|
||||
with pytest.raises(tmdb.TmdbError) as ei:
|
||||
client._get("/movie/1")
|
||||
assert "SECRETKEY" not in str(ei.value) and "401" in str(ei.value)
|
||||
|
||||
class _NetHttp:
|
||||
def get(self, url, params=None):
|
||||
raise httpx.ConnectError("connect failed", request=req)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
client._http = _NetHttp()
|
||||
with pytest.raises(tmdb.TmdbError) as ei2:
|
||||
client._get("/movie/1")
|
||||
assert "SECRETKEY" not in str(ei2.value)
|
||||
client.close()
|
||||
|
||||
|
||||
def test_run_enrich_noops_without_key(db):
|
||||
# No tmdb_api_key configured (default "") → clean no-op, never an error.
|
||||
out = tmdb.run_enrich(db)
|
||||
|
||||
Reference in New Issue
Block a user