test: no test throws a response away; the stream session is narrowed, not baselined
The 7 baselined Optional accesses in plex/stream.py were one fact the type did not carry: the initial spawn (replace_only=None) always supersedes, so only the fallback respawn can get None. An assert at the call site says so; the pyright baseline drops 195 -> 188. test_abuse_limits: the public-URL and YouTube enqueue tests stopped at the profile lookup (400 no_profile) and passed on that unrelated refusal. They now send an inline spec and assert the job is queued. The same shape — a request whose response is discarded — sat at 53 more places across the suite (all returned what they should, measured with a TestClient probe); each now asserts its status, and test_response_checked keeps it that way (seen red).
This commit is contained in:
@@ -548,64 +548,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"./app/plex/stream.py": [
|
|
||||||
{
|
|
||||||
"code": "reportOptionalMemberAccess",
|
|
||||||
"range": {
|
|
||||||
"startColumn": 51,
|
|
||||||
"endColumn": 55,
|
|
||||||
"lineCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "reportOptionalMemberAccess",
|
|
||||||
"range": {
|
|
||||||
"startColumn": 23,
|
|
||||||
"endColumn": 25,
|
|
||||||
"lineCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "reportOptionalMemberAccess",
|
|
||||||
"range": {
|
|
||||||
"startColumn": 18,
|
|
||||||
"endColumn": 31,
|
|
||||||
"lineCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "reportOptionalMemberAccess",
|
|
||||||
"range": {
|
|
||||||
"startColumn": 75,
|
|
||||||
"endColumn": 79,
|
|
||||||
"lineCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "reportOptionalMemberAccess",
|
|
||||||
"range": {
|
|
||||||
"startColumn": 10,
|
|
||||||
"endColumn": 23,
|
|
||||||
"lineCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "reportOptionalMemberAccess",
|
|
||||||
"range": {
|
|
||||||
"startColumn": 10,
|
|
||||||
"endColumn": 12,
|
|
||||||
"lineCount": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "reportOptionalMemberAccess",
|
|
||||||
"range": {
|
|
||||||
"startColumn": 10,
|
|
||||||
"endColumn": 17,
|
|
||||||
"lineCount": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"./app/plex/sync.py": [
|
"./app/plex/sync.py": [
|
||||||
{
|
{
|
||||||
"code": "reportArgumentType",
|
"code": "reportArgumentType",
|
||||||
|
|||||||
@@ -745,8 +745,7 @@ def start_session(
|
|||||||
# check-and-replace happens under ONE _lock, so a newer same-key session (a concurrent
|
# check-and-replace happens under ONE _lock, so a newer same-key session (a concurrent
|
||||||
# seek-restart) installed between the fallback's decision and here is never clobbered. When the
|
# seek-restart) installed between the fallback's decision and here is never clobbered. When the
|
||||||
# registered session is no longer `replace_only`, we abort and return None (the caller yields to
|
# registered session is no longer `replace_only`, we abort and return None (the caller yields to
|
||||||
# whoever is registered now). The INITIAL spawn passes replace_only=None and always supersedes —
|
# whoever is registered now). The INITIAL spawn never gets None — see its call site.
|
||||||
# that is the seek-restart's intended last-writer-wins.
|
|
||||||
# Software-fallback encode tuning — read only on the SW transcode path (the VAAPI path and
|
# Software-fallback encode tuning — read only on the SW transcode path (the VAAPI path and
|
||||||
# remux ignore preset/crf/max_height, and remux must not touch the DB for them).
|
# remux ignore preset/crf/max_height, and remux must not touch the DB for them).
|
||||||
preset, crf, max_height = "veryfast", 20, 0
|
preset, crf, max_height = "veryfast", 20, 0
|
||||||
@@ -823,6 +822,7 @@ def start_session(
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
s = _spawn(*_select_encode(db, item, transcode, src))
|
s = _spawn(*_select_encode(db, item, transcode, src))
|
||||||
|
assert s is not None # replace_only=None always supersedes: a seek-restart's last-writer-wins
|
||||||
# Wait for the first video segment outside the lock (this blocks on ffmpeg, and we must not hold up
|
# Wait for the first video segment outside the lock (this blocks on ffmpeg, and we must not hold up
|
||||||
# other sessions). Pass the proc so a HW session that dies at VAAPI init fails fast (not after 25s).
|
# other sessions). Pass the proc so a HW session that dies at VAAPI init fails fast (not after 25s).
|
||||||
ready = wait_for(_seg(0), timeout=25.0, proc=s.proc)
|
ready = wait_for(_seg(0), timeout=25.0, proc=s.proc)
|
||||||
|
|||||||
@@ -36,9 +36,14 @@ def test_a_public_url_gets_past_the_guard(db, as_user, make_user, monkeypatch):
|
|||||||
"getaddrinfo",
|
"getaddrinfo",
|
||||||
lambda *a, **kw: [(None, None, None, "", ("93.184.216.34", 0))],
|
lambda *a, **kw: [(None, None, None, "", ("93.184.216.34", 0))],
|
||||||
)
|
)
|
||||||
r = as_user(user).post("/api/downloads", json={"source": "https://example.com/v"})
|
# An inline spec, so nothing ELSE refuses it: without one the request stopped at the
|
||||||
detail = r.json().get("detail") if r.status_code >= 400 else None
|
# profile lookup (400 no_profile) and "not url_blocked" held for a request the guard never
|
||||||
assert not (isinstance(detail, dict) and detail.get("code") == "url_blocked")
|
# had to wave through (E32 S2).
|
||||||
|
r = as_user(user).post(
|
||||||
|
"/api/downloads", json={"source": "https://example.com/v", "spec": {"format": "best"}}
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["status"] == "queued"
|
||||||
|
|
||||||
|
|
||||||
def test_youtube_sources_never_consult_the_guard(db, as_user, make_user, monkeypatch):
|
def test_youtube_sources_never_consult_the_guard(db, as_user, make_user, monkeypatch):
|
||||||
@@ -50,7 +55,13 @@ def test_youtube_sources_never_consult_the_guard(db, as_user, make_user, monkeyp
|
|||||||
user = make_user()
|
user = make_user()
|
||||||
db.commit()
|
db.commit()
|
||||||
monkeypatch.setattr(urlguard.socket, "getaddrinfo", boom)
|
monkeypatch.setattr(urlguard.socket, "getaddrinfo", boom)
|
||||||
as_user(user).post("/api/downloads", json={"source": "dQw4w9WgXcQ"})
|
# The job must actually be created: a request refused for any other reason would never reach
|
||||||
|
# the guard either, and the test would pass without proving anything (E32 S2).
|
||||||
|
r = as_user(user).post(
|
||||||
|
"/api/downloads", json={"source": "dQw4w9WgXcQ", "spec": {"format": "best"}}
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["source_kind"] == "youtube"
|
||||||
|
|
||||||
|
|
||||||
def test_manual_subscription_sync_goes_on_cooldown(db, as_user, make_user, monkeypatch):
|
def test_manual_subscription_sync_goes_on_cooldown(db, as_user, make_user, monkeypatch):
|
||||||
@@ -81,8 +92,12 @@ def test_the_cooldown_is_per_user_not_global(db, as_user, make_user, monkeypatch
|
|||||||
monkeypatch.setattr(syncroutes, "import_subscriptions", lambda d, u: {"imported": 0})
|
monkeypatch.setattr(syncroutes, "import_subscriptions", lambda d, u: {"imported": 0})
|
||||||
|
|
||||||
ca = as_user(a)
|
ca = as_user(a)
|
||||||
for _ in range(4):
|
assert [ca.post("/api/sync/subscriptions").status_code for _ in range(4)] == [
|
||||||
ca.post("/api/sync/subscriptions")
|
200,
|
||||||
|
200,
|
||||||
|
200,
|
||||||
|
429,
|
||||||
|
]
|
||||||
assert ca.post("/api/sync/subscriptions").status_code == 429
|
assert ca.post("/api/sync/subscriptions").status_code == 429
|
||||||
assert as_user(b).post("/api/sync/subscriptions").status_code == 200
|
assert as_user(b).post("/api/sync/subscriptions").status_code == 200
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ def test_nobody_reaches_another_users_session(db, make_user, as_user):
|
|||||||
def test_the_list_is_the_users_own_newest_first(db, make_user, as_user):
|
def test_the_list_is_the_users_own_newest_first(db, make_user, as_user):
|
||||||
client = as_user(make_user())
|
client = as_user(make_user())
|
||||||
first = _start(client)["id"]
|
first = _start(client)["id"]
|
||||||
client.post(f"/api/debug-sessions/{first}/stop")
|
assert client.post(f"/api/debug-sessions/{first}/stop").status_code == 200
|
||||||
second = _start(client)["id"]
|
second = _start(client)["id"]
|
||||||
assert [s["id"] for s in client.get("/api/debug-sessions").json()["items"]] == [second, first]
|
assert [s["id"] for s in client.get("/api/debug-sessions").json()["items"]] == [second, first]
|
||||||
|
|
||||||
@@ -199,10 +199,13 @@ def test_an_erased_account_takes_its_sessions_with_it(db, make_user, as_user):
|
|||||||
user = make_user()
|
user = make_user()
|
||||||
client = as_user(user)
|
client = as_user(user)
|
||||||
sid = _start(client)["id"]
|
sid = _start(client)["id"]
|
||||||
client.post(
|
assert (
|
||||||
f"/api/debug-sessions/{sid}/screenshots",
|
client.post(
|
||||||
content=_PNG,
|
f"/api/debug-sessions/{sid}/screenshots",
|
||||||
headers={"content-type": "image/png"},
|
content=_PNG,
|
||||||
|
headers={"content-type": "image/png"},
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
purge_user(db, user, BackgroundTasks())
|
purge_user(db, user, BackgroundTasks())
|
||||||
assert db.get(DebugSession, sid) is None
|
assert db.get(DebugSession, sid) is None
|
||||||
@@ -250,7 +253,7 @@ def test_a_request_naming_the_own_open_session_is_tagged_and_raised(gated, db, c
|
|||||||
_signed_in(client, make_user)
|
_signed_in(client, make_user)
|
||||||
sid = _start(client, "DEBUG")["id"]
|
sid = _start(client, "DEBUG")["id"]
|
||||||
gated.records.clear()
|
gated.records.clear()
|
||||||
client.get("/api/debug-sessions", headers={"x-debug-session": sid})
|
assert client.get("/api/debug-sessions", headers={"x-debug-session": sid}).status_code == 200
|
||||||
(line,) = _request_lines(gated)
|
(line,) = _request_lines(gated)
|
||||||
assert getattr(line, "debug_session_id", None) == sid
|
assert getattr(line, "debug_session_id", None) == sid
|
||||||
# Raised to the session's level: the line is kept for storage though the user's own is Alap.
|
# Raised to the session's level: the line is kept for storage though the user's own is Alap.
|
||||||
@@ -263,7 +266,7 @@ def test_a_header_naming_anything_else_is_ignored_not_refused(gated, db, client,
|
|||||||
db.commit()
|
db.commit()
|
||||||
_signed_in(client, make_user)
|
_signed_in(client, make_user)
|
||||||
own = _start(client)["id"]
|
own = _start(client)["id"]
|
||||||
client.post(f"/api/debug-sessions/{own}/stop")
|
assert client.post(f"/api/debug-sessions/{own}/stop").status_code == 200
|
||||||
for value in (foreign, own, "not-a-session", "f" * 16, "A" * 16):
|
for value in (foreign, own, "not-a-session", "f" * 16, "A" * 16):
|
||||||
gated.records.clear()
|
gated.records.clear()
|
||||||
# Not `/api/me`: a polled route logs at DEBUG, and an un-raised request would drop the very
|
# Not `/api/me`: a polled route logs at DEBUG, and an un-raised request would drop the very
|
||||||
@@ -280,7 +283,7 @@ def test_a_header_naming_the_own_session_past_its_cap_is_ignored(gated, db, clie
|
|||||||
sid = _start(client)["id"]
|
sid = _start(client)["id"]
|
||||||
_expire(db, sid) # past its cap, not yet swept: still open in the table
|
_expire(db, sid) # past its cap, not yet swept: still open in the table
|
||||||
gated.records.clear()
|
gated.records.clear()
|
||||||
client.get("/api/debug-sessions", headers={"x-debug-session": sid})
|
assert client.get("/api/debug-sessions", headers={"x-debug-session": sid}).status_code == 200
|
||||||
(line,) = _request_lines(gated)
|
(line,) = _request_lines(gated)
|
||||||
assert getattr(line, "debug_session_id", None) is None
|
assert getattr(line, "debug_session_id", None) is None
|
||||||
assert getattr(line, "elevated", False) is False
|
assert getattr(line, "elevated", False) is False
|
||||||
@@ -616,7 +619,7 @@ def test_a_users_screenshots_are_capped_across_their_sessions(recording, monkeyp
|
|||||||
client, sid = recording
|
client, sid = recording
|
||||||
monkeypatch.setattr(debugsession, "SCREENSHOT_USER_MAX_BYTES", len(_PNG) * 2)
|
monkeypatch.setattr(debugsession, "SCREENSHOT_USER_MAX_BYTES", len(_PNG) * 2)
|
||||||
assert _upload(client, sid, _PNG).status_code == 200
|
assert _upload(client, sid, _PNG).status_code == 200
|
||||||
client.post(f"/api/debug-sessions/{sid}/stop")
|
assert client.post(f"/api/debug-sessions/{sid}/stop").status_code == 200
|
||||||
other = _start(client)["id"]
|
other = _start(client)["id"]
|
||||||
assert _upload(client, other, _PNG).status_code == 200
|
assert _upload(client, other, _PNG).status_code == 200
|
||||||
r = _upload(client, other, _PNG)
|
r = _upload(client, other, _PNG)
|
||||||
@@ -631,7 +634,7 @@ def test_new_sessions_are_paced_but_joining_one_is_not(db, make_user, as_user, m
|
|||||||
client = as_user(make_user())
|
client = as_user(make_user())
|
||||||
sid = _start(client)["id"]
|
sid = _start(client)["id"]
|
||||||
assert _start(client)["id"] == sid # another tab joining
|
assert _start(client)["id"] == sid # another tab joining
|
||||||
client.post(f"/api/debug-sessions/{sid}/stop")
|
assert client.post(f"/api/debug-sessions/{sid}/stop").status_code == 200
|
||||||
r = client.post("/api/debug-sessions", json={"level": "DEBUG"})
|
r = client.post("/api/debug-sessions", json={"level": "DEBUG"})
|
||||||
assert r.status_code == 429 and r.json()["detail"]["reason"] == "debug_session_rate_limited"
|
assert r.status_code == 429 and r.json()["detail"]["reason"] == "debug_session_rate_limited"
|
||||||
|
|
||||||
@@ -745,9 +748,16 @@ def test_the_export_is_the_saved_draft_the_evidence_and_the_screenshots(db, make
|
|||||||
db.commit()
|
db.commit()
|
||||||
_upload(client, sid, _PNG)
|
_upload(client, sid, _PNG)
|
||||||
_upload(client, sid, _JPEG)
|
_upload(client, sid, _JPEG)
|
||||||
client.put(
|
assert (
|
||||||
f"/api/debug-sessions/{sid}/draft",
|
client.put(
|
||||||
json={"title": "Spinner stays", "body": "It spins.", "excluded": [dropped_web.id, 999999]},
|
f"/api/debug-sessions/{sid}/draft",
|
||||||
|
json={
|
||||||
|
"title": "Spinner stays",
|
||||||
|
"body": "It spins.",
|
||||||
|
"excluded": [dropped_web.id, 999999],
|
||||||
|
},
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
|
|
||||||
r = client.get(f"/api/debug-sessions/{sid}/export")
|
r = client.get(f"/api/debug-sessions/{sid}/export")
|
||||||
@@ -1144,7 +1154,7 @@ def test_new_issues_are_paced_per_user(gitea_fake, db, make_user, as_user, monke
|
|||||||
first = _start(client)["id"]
|
first = _start(client)["id"]
|
||||||
_ready(client, first)
|
_ready(client, first)
|
||||||
assert client.post(f"/api/debug-sessions/{first}/issue").status_code == 200
|
assert client.post(f"/api/debug-sessions/{first}/issue").status_code == 200
|
||||||
client.post(f"/api/debug-sessions/{first}/stop")
|
assert client.post(f"/api/debug-sessions/{first}/stop").status_code == 200
|
||||||
second = _start(client)["id"]
|
second = _start(client)["id"]
|
||||||
_ready(client, second)
|
_ready(client, second)
|
||||||
r = client.post(f"/api/debug-sessions/{second}/issue")
|
r = client.post(f"/api/debug-sessions/{second}/issue")
|
||||||
|
|||||||
@@ -212,7 +212,12 @@ def test_a_timestamp_outside_the_consent_window_is_clamped(elevated):
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
future = (now + timedelta(days=365)).isoformat()
|
future = (now + timedelta(days=365)).isoformat()
|
||||||
past = (now - timedelta(days=30)).isoformat()
|
past = (now - timedelta(days=30)).isoformat()
|
||||||
client.post("/api/diagnostics/events", json={"events": [_event(ts=future), _event(ts=past)]})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/api/diagnostics/events", json={"events": [_event(ts=future), _event(ts=past)]}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
late, early = rows()
|
late, early = rows()
|
||||||
assert late.ts <= datetime.now(timezone.utc)
|
assert late.ts <= datetime.now(timezone.utc)
|
||||||
assert early.ts >= now - timedelta(hours=diagnostics.ELEVATION_HOURS, minutes=1)
|
assert early.ts >= now - timedelta(hours=diagnostics.ELEVATION_HOURS, minutes=1)
|
||||||
@@ -342,15 +347,15 @@ def _app() -> TestClient:
|
|||||||
|
|
||||||
|
|
||||||
def test_the_account_reaches_the_handlers_own_lines(gated):
|
def test_the_account_reaches_the_handlers_own_lines(gated):
|
||||||
_app().get("/api/thing/41")
|
assert _app().get("/api/thing/41").status_code == 200
|
||||||
line = next(r for r in gated.records if r.getMessage() == "an ordinary line")
|
line = next(r for r in gated.records if r.getMessage() == "an ordinary line")
|
||||||
assert getattr(line, "user_id", None) == 41
|
assert getattr(line, "user_id", None) == 41
|
||||||
|
|
||||||
|
|
||||||
def test_one_users_elevation_logs_their_requests_in_detail_and_nobody_elses(gated):
|
def test_one_users_elevation_logs_their_requests_in_detail_and_nobody_elses(gated):
|
||||||
client = _app()
|
client = _app()
|
||||||
client.get("/api/thing/7", params={"level": "DEBUG"})
|
assert client.get("/api/thing/7", params={"level": "DEBUG"}).status_code == 200
|
||||||
client.get("/api/thing/8")
|
assert client.get("/api/thing/8").status_code == 200
|
||||||
detail = [r for r in gated.records if r.getMessage().startswith("detail only")]
|
detail = [r for r in gated.records if r.getMessage().startswith("detail only")]
|
||||||
assert [r.user_id for r in detail] == [7]
|
assert [r.user_id for r in detail] == [7]
|
||||||
assert getattr(detail[0], "elevated", False) is True
|
assert getattr(detail[0], "elevated", False) is True
|
||||||
@@ -363,9 +368,9 @@ def test_one_users_elevation_logs_their_requests_in_detail_and_nobody_elses(gate
|
|||||||
|
|
||||||
def test_the_root_level_returns_to_the_base_when_the_elevation_ends(gated):
|
def test_the_root_level_returns_to_the_base_when_the_elevation_ends(gated):
|
||||||
client = _app()
|
client = _app()
|
||||||
client.get("/api/thing/7", params={"level": "TRACE"})
|
assert client.get("/api/thing/7", params={"level": "TRACE"}).status_code == 200
|
||||||
assert logging.getLogger().level == logs.TRACE
|
assert logging.getLogger().level == logs.TRACE
|
||||||
client.get("/api/thing/7") # the same user, back at the base
|
assert client.get("/api/thing/7").status_code == 200 # the same user, back at the base
|
||||||
assert logging.getLogger().level == logging.INFO
|
assert logging.getLogger().level == logging.INFO
|
||||||
logs._track_elevation(9, logging.DEBUG, datetime.now(timezone.utc) - timedelta(seconds=1))
|
logs._track_elevation(9, logging.DEBUG, datetime.now(timezone.utc) - timedelta(seconds=1))
|
||||||
assert logging.getLogger().level == logging.INFO # an already-expired one is pruned at once
|
assert logging.getLogger().level == logging.INFO # an already-expired one is pruned at once
|
||||||
@@ -373,15 +378,25 @@ def test_the_root_level_returns_to_the_base_when_the_elevation_ends(gated):
|
|||||||
|
|
||||||
def test_the_query_string_is_logged_only_for_a_traced_request_and_redacted(gated):
|
def test_the_query_string_is_logged_only_for_a_traced_request_and_redacted(gated):
|
||||||
client = _app()
|
client = _app()
|
||||||
client.get("/api/thing/7", params={"level": "DEBUG", "q": "lofi"})
|
assert client.get("/api/thing/7", params={"level": "DEBUG", "q": "lofi"}).status_code == 200
|
||||||
client.get("/api/thing/7", params={"level": "TRACE", "q": "lofi", "token": "SECRET"})
|
assert (
|
||||||
|
client.get(
|
||||||
|
"/api/thing/7", params={"level": "TRACE", "q": "lofi", "token": "SECRET"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
lines = [r for r in gated.records if getattr(r, "event", None) == "http.request"]
|
lines = [r for r in gated.records if getattr(r, "event", None) == "http.request"]
|
||||||
assert "query" not in lines[0].attrs
|
assert "query" not in lines[0].attrs
|
||||||
assert "q=lofi" in lines[1].attrs["query"] and "SECRET" not in lines[1].attrs["query"]
|
assert "q=lofi" in lines[1].attrs["query"] and "SECRET" not in lines[1].attrs["query"]
|
||||||
|
|
||||||
|
|
||||||
def test_an_address_typed_into_a_search_is_redacted_though_the_browser_encodes_it(gated):
|
def test_an_address_typed_into_a_search_is_redacted_though_the_browser_encodes_it(gated):
|
||||||
_app().get("/api/thing/7?level=TRACE&q=someone%40example.com&new_password=hunter2")
|
assert (
|
||||||
|
_app()
|
||||||
|
.get("/api/thing/7?level=TRACE&q=someone%40example.com&new_password=hunter2")
|
||||||
|
.status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
(line,) = [r for r in gated.records if getattr(r, "event", None) == "http.request"]
|
(line,) = [r for r in gated.records if getattr(r, "event", None) == "http.request"]
|
||||||
assert line.attrs["query"] == "level=TRACE&q=[email]&new_password=[redacted]"
|
assert line.attrs["query"] == "level=TRACE&q=[email]&new_password=[redacted]"
|
||||||
|
|
||||||
@@ -400,7 +415,7 @@ def test_a_credential_param_goes_whole_however_its_value_decodes(raw):
|
|||||||
|
|
||||||
def test_an_admin_level_of_trace_does_not_log_other_users_query_strings(gated):
|
def test_an_admin_level_of_trace_does_not_log_other_users_query_strings(gated):
|
||||||
logs.set_level("TRACE")
|
logs.set_level("TRACE")
|
||||||
_app().get("/api/thing/7", params={"q": "something private"})
|
assert _app().get("/api/thing/7", params={"q": "something private"}).status_code == 200
|
||||||
(line,) = [r for r in gated.records if getattr(r, "event", None) == "http.request"]
|
(line,) = [r for r in gated.records if getattr(r, "event", None) == "http.request"]
|
||||||
assert "query" not in line.attrs
|
assert "query" not in line.attrs
|
||||||
|
|
||||||
@@ -488,7 +503,7 @@ def test_a_real_sign_in_carries_the_consented_level_into_the_process(gated, clie
|
|||||||
user = _signed_in(client, make_user)
|
user = _signed_in(client, make_user)
|
||||||
assert client.put("/api/me/diagnostics", json={"level": "DEBUG"}).status_code == 200
|
assert client.put("/api/me/diagnostics", json={"level": "DEBUG"}).status_code == 200
|
||||||
logs._elevations.clear() # only a request resolving the account through auth may put it back
|
logs._elevations.clear() # only a request resolving the account through auth may put it back
|
||||||
client.get("/api/me")
|
assert client.get("/api/me").status_code == 200
|
||||||
assert logs._elevations[user.id][0] == logging.DEBUG
|
assert logs._elevations[user.id][0] == logging.DEBUG
|
||||||
assert client.put("/api/me/diagnostics", json={"level": "INFO"}).status_code == 200
|
assert client.put("/api/me/diagnostics", json={"level": "INFO"}).status_code == 200
|
||||||
assert user.id not in logs._elevations
|
assert user.id not in logs._elevations
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ def test_me_reports_the_google_address_only_when_it_differs(client, db, make_use
|
|||||||
)
|
)
|
||||||
assert client.get("/api/me").json()["google_email"] is None
|
assert client.get("/api/me").json()["google_email"] is None
|
||||||
|
|
||||||
client.post("/auth/logout")
|
assert client.post("/auth/logout").status_code == 200
|
||||||
tagged = make_user(email="me+siftlode@gmail.com", password=_UAT_PW)
|
tagged = make_user(email="me+siftlode@gmail.com", password=_UAT_PW)
|
||||||
tagged.google_email = "me@gmail.com"
|
tagged.google_email = "me@gmail.com"
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -100,7 +100,12 @@ def test_the_admin_user_list_carries_last_login(as_user, db, make_user):
|
|||||||
def test_the_account_sees_its_own_last_sign_in(client, db, make_user):
|
def test_the_account_sees_its_own_last_sign_in(client, db, make_user):
|
||||||
# Settings → Account shows it (prod UAT 2026-09-19: only the admin's user list did).
|
# Settings → Account shows it (prod UAT 2026-09-19: only the admin's user list did).
|
||||||
make_user(email="me@example.com", password=_PW)
|
make_user(email="me@example.com", password=_PW)
|
||||||
client.post("/auth/password-login", json={"email": "me@example.com", "password": _PW})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/password-login", json={"email": "me@example.com", "password": _PW}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
stamp = client.get("/api/me").json()["last_login_at"]
|
stamp = client.get("/api/me").json()["last_login_at"]
|
||||||
|
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ def test_a_signed_in_request_is_never_capped_and_names_its_account(sink):
|
|||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
for _ in range(logs.ANONYMOUS_LINES_PER_MINUTE + 3):
|
for _ in range(logs.ANONYMOUS_LINES_PER_MINUTE + 3):
|
||||||
client.get("/api/me")
|
assert client.get("/api/me").status_code == 200
|
||||||
records = sink.records[-(logs.ANONYMOUS_LINES_PER_MINUTE + 3) :]
|
records = sink.records[-(logs.ANONYMOUS_LINES_PER_MINUTE + 3) :]
|
||||||
assert all(getattr(r, logs.PERSIST, True) for r in records)
|
assert all(getattr(r, logs.PERSIST, True) for r in records)
|
||||||
assert sink.objects()[-1]["user_id"] == 42 and "user_id" not in sink.objects()[-1]["attrs"]
|
assert sink.objects()[-1]["user_id"] == 42 and "user_id" not in sink.objects()[-1]["attrs"]
|
||||||
@@ -325,9 +325,9 @@ def test_the_log_consoles_own_reads_are_debug_but_its_failures_are_not(sink):
|
|||||||
http = logging.getLogger("siftlode.http")
|
http = logging.getLogger("siftlode.http")
|
||||||
http.setLevel(logs.TRACE)
|
http.setLevel(logs.TRACE)
|
||||||
client = _client()
|
client = _client()
|
||||||
client.get("/api/admin/logs/facets")
|
assert client.get("/api/admin/logs/facets").status_code == 200
|
||||||
assert sink.objects()[-1]["level"] == "DEBUG"
|
assert sink.objects()[-1]["level"] == "DEBUG"
|
||||||
client.get("/api/admin/logs/facets?fail=1")
|
assert client.get("/api/admin/logs/facets?fail=1").status_code == 400
|
||||||
assert sink.objects()[-1]["level"] == "WARNING"
|
assert sink.objects()[-1]["level"] == "WARNING"
|
||||||
|
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ def test_a_polled_read_is_debug_a_write_to_the_same_route_is_not(sink):
|
|||||||
r = client.post("/api/messages/unread_count")
|
r = client.post("/api/messages/unread_count")
|
||||||
assert sink.objects()[-1]["level"] == "INFO"
|
assert sink.objects()[-1]["level"] == "INFO"
|
||||||
assert "X-Log-Quiet" not in r.headers
|
assert "X-Log-Quiet" not in r.headers
|
||||||
client.get("/api/videos/abc")
|
assert client.get("/api/videos/abc").status_code == 200
|
||||||
assert sink.objects()[-1]["level"] == "INFO"
|
assert sink.objects()[-1]["level"] == "INFO"
|
||||||
|
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ def test_every_polled_route_is_a_route_the_app_has():
|
|||||||
|
|
||||||
def test_a_failing_request_is_logged_by_status_and_an_unhandled_error_is_logged_at_all(sink):
|
def test_a_failing_request_is_logged_by_status_and_an_unhandled_error_is_logged_at_all(sink):
|
||||||
client = _client()
|
client = _client()
|
||||||
client.get("/api/videos/missing/deeper") # 404 from the router
|
assert client.get("/api/videos/missing/deeper").status_code == 404 # 404 from the router
|
||||||
assert sink.objects()[-1]["level"] == "WARNING"
|
assert sink.objects()[-1]["level"] == "WARNING"
|
||||||
client.get("/boom")
|
client.get("/boom")
|
||||||
payload = sink.objects()[-1]
|
payload = sink.objects()[-1]
|
||||||
|
|||||||
@@ -154,8 +154,18 @@ def test_an_access_request_records_the_language_it_was_made_in(db, client):
|
|||||||
|
|
||||||
|
|
||||||
def test_asking_again_from_another_language_updates_the_row(db, client):
|
def test_asking_again_from_another_language_updates_the_row(db, client):
|
||||||
client.post("/auth/request-access", json={"email": "asking@example.com", "lang": "en"})
|
assert (
|
||||||
client.post("/auth/request-access", json={"email": "asking@example.com", "lang": "hu"})
|
client.post(
|
||||||
|
"/auth/request-access", json={"email": "asking@example.com", "lang": "en"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/request-access", json={"email": "asking@example.com", "lang": "hu"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
||||||
@@ -165,7 +175,12 @@ def test_asking_again_from_another_language_updates_the_row(db, client):
|
|||||||
|
|
||||||
|
|
||||||
def test_an_unsupported_language_is_ignored_rather_than_stored(db, client):
|
def test_an_unsupported_language_is_ignored_rather_than_stored(db, client):
|
||||||
client.post("/auth/request-access", json={"email": "asking@example.com", "lang": "../../etc"})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/request-access", json={"email": "asking@example.com", "lang": "../../etc"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
||||||
assert inv.lang is None
|
assert inv.lang is None
|
||||||
@@ -194,7 +209,12 @@ def test_the_confirmation_mail_is_written_in_the_accounts_language(db, client, m
|
|||||||
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: True)
|
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: True)
|
||||||
sent = _bodies(monkeypatch)
|
sent = _bodies(monkeypatch)
|
||||||
|
|
||||||
client.post("/auth/register", json={"email": "hu@example.com", "password": _PW, "lang": "hu"})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/register", json={"email": "hu@example.com", "password": _PW, "lang": "hu"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
(_, subject, body) = sent[0]
|
(_, subject, body) = sent[0]
|
||||||
assert subject == mailtext.MAIL_TEXT["verify_email.subject"]["hu"]
|
assert subject == mailtext.MAIL_TEXT["verify_email.subject"]["hu"]
|
||||||
@@ -209,7 +229,12 @@ def test_the_approval_mail_answers_in_the_language_the_request_was_made_in(
|
|||||||
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: True)
|
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: True)
|
||||||
sent = _bodies(monkeypatch)
|
sent = _bodies(monkeypatch)
|
||||||
admin = make_user(email="admin@example.com", role="admin")
|
admin = make_user(email="admin@example.com", role="admin")
|
||||||
client.post("/auth/request-access", json={"email": "asking@example.com", "lang": "hu"})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/request-access", json={"email": "asking@example.com", "lang": "hu"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
||||||
sent.clear() # drop the admin notification; this test is about the reply to the requester
|
sent.clear() # drop the admin notification; this test is about the reply to the requester
|
||||||
|
|
||||||
@@ -227,11 +252,16 @@ def test_a_denial_answers_in_that_language_too(db, client, make_user, as_user, m
|
|||||||
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: True)
|
monkeypatch.setattr(email_mod, "email_enabled", lambda db=None: True)
|
||||||
sent = _bodies(monkeypatch)
|
sent = _bodies(monkeypatch)
|
||||||
admin = make_user(email="admin@example.com", role="admin")
|
admin = make_user(email="admin@example.com", role="admin")
|
||||||
client.post("/auth/request-access", json={"email": "asking@example.com", "lang": "hu"})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/request-access", json={"email": "asking@example.com", "lang": "hu"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
inv = db.execute(select(Invite).where(Invite.email == "asking@example.com")).scalar_one()
|
||||||
sent.clear()
|
sent.clear()
|
||||||
|
|
||||||
as_user(admin).post(f"/api/admin/invites/{inv.id}/deny")
|
assert as_user(admin).post(f"/api/admin/invites/{inv.id}/deny").status_code == 200
|
||||||
|
|
||||||
assert sent[0][1] == mailtext.MAIL_TEXT["access_denied.subject"]["hu"]
|
assert sent[0][1] == mailtext.MAIL_TEXT["access_denied.subject"]["hu"]
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ def test_putting_a_removed_video_back_leaves_a_linked_list_clean(db, make_user,
|
|||||||
pl.synced_fingerprint = current_fingerprint(db, pl)
|
pl.synced_fingerprint = current_fingerprint(db, pl)
|
||||||
pl.dirty = False
|
pl.dirty = False
|
||||||
db.commit()
|
db.commit()
|
||||||
client.delete(f"/api/playlists/{pl.id}/items/b")
|
assert client.delete(f"/api/playlists/{pl.id}/items/b").status_code == 200
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
assert db.get(Playlist, pl.id).dirty is True
|
assert db.get(Playlist, pl.id).dirty is True
|
||||||
_add(client, pl.id, {"video_id": "b", "position": 1})
|
_add(client, pl.id, {"video_id": "b", "position": 1})
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def test_each_variant_is_cut_at_its_own_size(db, make_user, monkeypatch, tmp_pat
|
|||||||
]
|
]
|
||||||
# Each cut is cached under its own name: asking again reaches Plex no more.
|
# Each cut is cached under its own name: asking again reaches Plex no more.
|
||||||
for variant in ("thumb", "thumb_sm", "art", "art_sm"):
|
for variant in ("thumb", "thumb_sm", "art", "art_sm"):
|
||||||
client.get(f"/api/plex/image/pm1?variant={variant}")
|
assert client.get(f"/api/plex/image/pm1?variant={variant}").status_code == 200
|
||||||
assert len(_FakePlex.calls) == 4
|
assert len(_FakePlex.calls) == 4
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -105,8 +105,11 @@ def test_remove_then_put_back_is_no_change(db, make_user, as_user):
|
|||||||
user = make_user()
|
user = make_user()
|
||||||
pl = _seed(db, user, ["a", "b", "c", "d"], extra=())
|
pl = _seed(db, user, ["a", "b", "c", "d"], extra=())
|
||||||
client = as_user(user)
|
client = as_user(user)
|
||||||
client.post(
|
assert (
|
||||||
f"/api/plex/playlists/{pl.id}/items/remove-bulk", json={"item_rating_keys": ["b", "d"]}
|
client.post(
|
||||||
|
f"/api/plex/playlists/{pl.id}/items/remove-bulk", json={"item_rating_keys": ["b", "d"]}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
assert _order(db, pl.id)[0] == ["a", "c"]
|
assert _order(db, pl.id)[0] == ["a", "c"]
|
||||||
_add(client, pl.id, ["b", "d"], [1, 3])
|
_add(client, pl.id, ["b", "d"], [1, 3])
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ def test_an_unshowable_track_says_why_and_maps_to_a_status(client, monkeypatch):
|
|||||||
raise subtitles.SubtitleUnavailable("bitmap")
|
raise subtitles.SubtitleUnavailable("bitmap")
|
||||||
|
|
||||||
monkeypatch.setattr(subtitles, "_extract_and_publish", _bitmap)
|
monkeypatch.setattr(subtitles, "_extract_and_publish", _bitmap)
|
||||||
client.get("/api/plex/subtitle/98614/0/status")
|
assert client.get("/api/plex/subtitle/98614/0/status").status_code == 200
|
||||||
pool.run_all()
|
pool.run_all()
|
||||||
st = client.get("/api/plex/subtitle/98614/0/status").json()
|
st = client.get("/api/plex/subtitle/98614/0/status").json()
|
||||||
assert st["state"] == "failed" and st["code"] == "bitmap"
|
assert st["state"] == "failed" and st["code"] == "bitmap"
|
||||||
|
|||||||
@@ -51,7 +51,12 @@ def test_a_link_whose_account_was_deleted_is_reported_dead(client, db, make_user
|
|||||||
|
|
||||||
def test_a_spent_link_is_reported_dead(client, db, make_user):
|
def test_a_spent_link_is_reported_dead(client, db, make_user):
|
||||||
raw = _issue(db, make_user(email="spent@example.com", password=_PW))
|
raw = _issue(db, make_user(email="spent@example.com", password=_PW))
|
||||||
client.post("/auth/password-reset/confirm", json={"token": raw, "password": _NEW})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/password-reset/confirm", json={"token": raw, "password": _NEW}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
assert client.post("/auth/password-reset/check", json={"token": raw}).json() == {"ok": False}
|
assert client.post("/auth/password-reset/check", json={"token": raw}).json() == {"ok": False}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""No test sends a request and throws the response away (E32 S2).
|
||||||
|
|
||||||
|
A discarded `client.post(...)` passes whatever it got back. As a SETUP step, a failure leaves the
|
||||||
|
test asserting on a state it never reached; as the step UNDER test, the test proves nothing. E32
|
||||||
|
found `test_youtube_sources_never_consult_the_guard` passing on a 400 that stopped the request
|
||||||
|
before the code it meant to exercise, and 53 more discarded responses across the suite. Each one
|
||||||
|
now states the status it expects; this keeps it that way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TESTS = Path(__file__).resolve().parent
|
||||||
|
VERBS = {"get", "post", "put", "patch", "delete", "head", "options", "request"}
|
||||||
|
|
||||||
|
|
||||||
|
def discarded_requests(source: str) -> list[int]:
|
||||||
|
"""Line numbers of the statements that are a bare request call on an app path."""
|
||||||
|
hits = []
|
||||||
|
for node in ast.walk(ast.parse(source)):
|
||||||
|
if not (isinstance(node, ast.Expr) and isinstance(node.value, ast.Call)):
|
||||||
|
continue
|
||||||
|
func = node.value.func
|
||||||
|
if not (isinstance(func, ast.Attribute) and func.attr in VERBS):
|
||||||
|
continue
|
||||||
|
path = node.value.args[0] if node.value.args else None
|
||||||
|
literal = path.value if isinstance(path, ast.Constant) else None
|
||||||
|
if isinstance(path, ast.JoinedStr):
|
||||||
|
first = path.values[0] if path.values else None
|
||||||
|
literal = first.value if isinstance(first, ast.Constant) else None
|
||||||
|
if isinstance(literal, str) and literal.startswith(("/api", "/auth")):
|
||||||
|
hits.append(node.lineno)
|
||||||
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_detector_sees_a_discarded_request_and_passes_a_checked_one():
|
||||||
|
src = (
|
||||||
|
"client.post('/api/x')\n"
|
||||||
|
"client.get(f'/auth/{u}')\n"
|
||||||
|
"assert client.post('/api/x').status_code == 200\n"
|
||||||
|
"r = client.get('/api/y')\n"
|
||||||
|
"items.get('/api/not-a-request-but-a-key')\n"
|
||||||
|
)
|
||||||
|
# The last line is a false positive by design: a dict keyed by an app path is not a thing the
|
||||||
|
# suite does, and a narrower match would miss a client held under any other name.
|
||||||
|
assert discarded_requests(src) == [1, 2, 5]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_test_discards_a_response():
|
||||||
|
offenders = [
|
||||||
|
f"{p.name}:{line}"
|
||||||
|
for p in sorted(TESTS.rglob("test_*.py"))
|
||||||
|
for line in discarded_requests(p.read_text(encoding="utf-8"))
|
||||||
|
]
|
||||||
|
assert offenders == [], (
|
||||||
|
"assert the status these requests expect (`assert client.post(...).status_code == 200`), "
|
||||||
|
f"or keep the response and check it: {offenders}"
|
||||||
|
)
|
||||||
@@ -100,15 +100,22 @@ def test_a_later_registration_never_touches_an_existing_password(db, client, smt
|
|||||||
have installed a password where the whole point is that there is none. Reverted; the owner's
|
have installed a password where the whole point is that there is none. Reverted; the owner's
|
||||||
recourse is password-reset, which proves the mailbox. Residual logged as SEC-16.
|
recourse is password-reset, which proves the mailbox. Residual logged as SEC-16.
|
||||||
"""
|
"""
|
||||||
client.post(
|
assert (
|
||||||
"/auth/register", json={"email": "victim@example.com", "password": "the first password"}
|
client.post(
|
||||||
|
"/auth/register", json={"email": "victim@example.com", "password": "the first password"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
user = db.execute(select(User).where(User.email == "victim@example.com")).scalar_one()
|
user = db.execute(select(User).where(User.email == "victim@example.com")).scalar_one()
|
||||||
before = user.password_hash
|
before = user.password_hash
|
||||||
|
|
||||||
client.post(
|
assert (
|
||||||
"/auth/register", json={"email": "victim@example.com", "password": "the second password"}
|
client.post(
|
||||||
|
"/auth/register",
|
||||||
|
json={"email": "victim@example.com", "password": "the second password"},
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
@@ -116,8 +123,11 @@ def test_a_later_registration_never_touches_an_existing_password(db, client, smt
|
|||||||
|
|
||||||
|
|
||||||
def test_a_later_registration_leaves_a_link_already_in_flight_alive(db, client, smtp_on, wire):
|
def test_a_later_registration_leaves_a_link_already_in_flight_alive(db, client, smtp_on, wire):
|
||||||
client.post(
|
assert (
|
||||||
"/auth/register", json={"email": "victim@example.com", "password": "the first password"}
|
client.post(
|
||||||
|
"/auth/register", json={"email": "victim@example.com", "password": "the first password"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
user = db.execute(select(User).where(User.email == "victim@example.com")).scalar_one()
|
user = db.execute(select(User).where(User.email == "victim@example.com")).scalar_one()
|
||||||
@@ -126,8 +136,12 @@ def test_a_later_registration_leaves_a_link_already_in_flight_alive(db, client,
|
|||||||
# would go green while asserting nothing the day registration stops issuing a token.
|
# would go green while asserting nothing the day registration stops issuing a token.
|
||||||
assert len(first) == 1
|
assert len(first) == 1
|
||||||
|
|
||||||
client.post(
|
assert (
|
||||||
"/auth/register", json={"email": "victim@example.com", "password": "the second password"}
|
client.post(
|
||||||
|
"/auth/register",
|
||||||
|
json={"email": "victim@example.com", "password": "the second password"},
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
@@ -148,8 +162,12 @@ def test_a_stripped_account_keeps_its_recovery_stamp(db, client, smtp_on, make_u
|
|||||||
password_revoked_at=datetime.now(timezone.utc),
|
password_revoked_at=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
|
|
||||||
client.post(
|
assert (
|
||||||
"/auth/register", json={"email": "stripped@example.com", "password": "an attacker password"}
|
client.post(
|
||||||
|
"/auth/register",
|
||||||
|
json={"email": "stripped@example.com", "password": "an attacker password"},
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
@@ -169,7 +187,9 @@ def test_a_denied_sibling_is_not_reopened_while_a_pending_row_exists(db, client)
|
|||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
client.post("/auth/request-access", json={"email": "pairone@gmail.com"})
|
assert (
|
||||||
|
client.post("/auth/request-access", json={"email": "pairone@gmail.com"}).status_code == 200
|
||||||
|
)
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
rows = {i.email: i.status for i in db.execute(select(Invite)).scalars().all()}
|
rows = {i.email: i.status for i in db.execute(select(Invite)).scalars().all()}
|
||||||
@@ -195,7 +215,7 @@ def test_an_env_granted_address_cannot_be_shadowed_by_a_stranger(db, client, mon
|
|||||||
# env grant. With the env list matched canonically the request never opens a row.
|
# env grant. With the env list matched canonically the request never opens a row.
|
||||||
monkeypatch.setattr(settings, "allowed_emails", "bob@gmail.com")
|
monkeypatch.setattr(settings, "allowed_emails", "bob@gmail.com")
|
||||||
|
|
||||||
client.post("/auth/request-access", json={"email": "b.ob@gmail.com"})
|
assert client.post("/auth/request-access", json={"email": "b.ob@gmail.com"}).status_code == 200
|
||||||
|
|
||||||
assert db.execute(select(Invite)).scalars().all() == []
|
assert db.execute(select(Invite)).scalars().all() == []
|
||||||
assert auth_core.is_allowed(db, "bob@gmail.com") is True
|
assert auth_core.is_allowed(db, "bob@gmail.com") is True
|
||||||
@@ -331,7 +351,12 @@ def test_two_admins_two_languages_two_mails(db, client, make_user, smtp_on, wire
|
|||||||
make_user(email="hu.admin@example.com", role="admin", preferences={"language": "hu"})
|
make_user(email="hu.admin@example.com", role="admin", preferences={"language": "hu"})
|
||||||
make_user(email="en.admin@example.com", role="admin", preferences={"language": "en"})
|
make_user(email="en.admin@example.com", role="admin", preferences={"language": "en"})
|
||||||
|
|
||||||
client.post("/auth/request-access", json={"email": "asking@example.com", "lang": "en"})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/request-access", json={"email": "asking@example.com", "lang": "en"}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
notices = [w for w in wire if "asking@example.com" in w[1]]
|
notices = [w for w in wire if "asking@example.com" in w[1]]
|
||||||
assert len(notices) == 2
|
assert len(notices) == 2
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ def test_default_is_per_source(db, monkeypatch, override_dep):
|
|||||||
pid = c.post("/api/saved-views", json={"name": "P", "filters": {}, "source": "plex"}).json()[
|
pid = c.post("/api/saved-views", json={"name": "P", "filters": {}, "source": "plex"}).json()[
|
||||||
"id"
|
"id"
|
||||||
]
|
]
|
||||||
c.patch(f"/api/saved-views/{fid}", json={"is_default": True})
|
assert c.patch(f"/api/saved-views/{fid}", json={"is_default": True}).status_code == 200
|
||||||
c.patch(f"/api/saved-views/{pid}", json={"is_default": True})
|
assert c.patch(f"/api/saved-views/{pid}", json={"is_default": True}).status_code == 200
|
||||||
# Making the Plex view default must NOT clear the feed view's default (different source).
|
# Making the Plex view default must NOT clear the feed view's default (different source).
|
||||||
assert c.get("/api/saved-views?source=feed").json()[0]["is_default"] is True
|
assert c.get("/api/saved-views?source=feed").json()[0]["is_default"] is True
|
||||||
assert c.get("/api/saved-views?source=plex").json()[0]["is_default"] is True
|
assert c.get("/api/saved-views?source=plex").json()[0]["is_default"] is True
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ def test_using_the_link_evicts_whoever_held_the_old_password(db, as_user, make_u
|
|||||||
admin = make_user(role="admin")
|
admin = make_user(role="admin")
|
||||||
db.commit()
|
db.commit()
|
||||||
token = _link(as_user(admin), target.id)
|
token = _link(as_user(admin), target.id)
|
||||||
client.post(
|
assert (
|
||||||
"/auth/password-reset/confirm", json={"token": token, "password": "the owner's own choice"}
|
client.post(
|
||||||
|
"/auth/password-reset/confirm",
|
||||||
|
json={"token": token, "password": "the owner's own choice"},
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
)
|
)
|
||||||
|
|
||||||
db.refresh(target)
|
db.refresh(target)
|
||||||
|
|||||||
@@ -229,7 +229,12 @@ def test_registering_again_does_not_create_a_second_account(db, client, monkeypa
|
|||||||
_smtp_on(monkeypatch)
|
_smtp_on(monkeypatch)
|
||||||
_capture(monkeypatch, "send_verify_email")
|
_capture(monkeypatch, "send_verify_email")
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
client.post("/auth/register", json={"email": "twice@example.com", "password": _PW})
|
assert (
|
||||||
|
client.post(
|
||||||
|
"/auth/register", json={"email": "twice@example.com", "password": _PW}
|
||||||
|
).status_code
|
||||||
|
== 200
|
||||||
|
)
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
rows = db.execute(select(User).where(User.email == "twice@example.com")).scalars().all()
|
rows = db.execute(select(User).where(User.email == "twice@example.com")).scalars().all()
|
||||||
|
|||||||
Reference in New Issue
Block a user