Files
siftlode/backend/tests/test_links.py
T
peter 33dbf70313 test(backend): pure-logic pytest harness, run in an isolated test image
The backend had zero tests. This adds 33, all pure (no DB, no network), plus the
harness to run them in the gate:

- backend/Dockerfile.test: the app's deps + pinned pytest/ruff, code bind-mounted at
  run time so it always tests the working tree. Isolated from the prod Dockerfile, so
  the published image never carries test tooling.
- requirements-dev.txt: pytest==8.4.2, ruff==0.15.21 (the version the gate already
  runs) — the backend lane is now pinned, not just host-global.
- tests: normalize_title (de-shout, hashtag strip, emoji drop, trilingual letters
  survive); storage sanitize/rel_path/download_filename and the safe_abs_path
  traversal guard (../ and absolute-path escapes rejected — verified by mutation);
  links HMAC grants (round-trip, wrong-token, tampered sig/exp, expiry) + is_expired;
  a DB-free app-assembly smoke (every router wires up, OpenAPI generates).

DB-backed router smoke (auth/feed/downloads) needs a test Postgres + migrations —
deferred. useCardPager needs hook-test infra (jsdom/renderHook) — deferred.
2026-07-21 03:29:24 +02:00

54 lines
2.2 KiB
Python

"""Signed-grant HMAC for password-protected share links: a grant proves the viewer unlocked a
token, rides in the file URL (a <video src> can't send a header), and expires."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from app.downloads.links import _GRANT_TTL, check_grant, is_expired, make_grant, new_token
def test_new_token_is_unguessable_and_unique():
a, b = new_token(), new_token()
assert a != b
assert len(a) >= 30 # token_urlsafe(24) → ~32 chars
class TestGrant:
def test_a_fresh_grant_verifies_for_its_own_token(self):
grant = make_grant("tok")
assert check_grant("tok", grant) is True
def test_a_grant_does_not_verify_for_a_different_token(self):
# The signature binds the token, so a grant minted for one link can't unlock another.
assert check_grant("other", make_grant("tok")) is False
def test_a_tampered_signature_is_rejected(self):
exp, _sig = make_grant("tok").split(".", 1)
assert check_grant("tok", f"{exp}.deadbeef") is False
def test_a_tampered_expiry_is_rejected(self):
# Extending the exp field invalidates the signature (which covers token.exp).
_exp, sig = make_grant("tok").split(".", 1)
future = int(datetime.now(timezone.utc).timestamp()) + 10 * _GRANT_TTL
assert check_grant("tok", f"{future}.{sig}") is False
def test_an_expired_grant_is_rejected(self):
past = datetime.now(timezone.utc).timestamp() - 2 * _GRANT_TTL
assert check_grant("tok", make_grant("tok", now=past)) is False
def test_malformed_grants_are_rejected_not_crashed(self):
for bad in [None, "", "no-dot", "notanumber.sig"]:
assert check_grant("tok", bad) is False
class TestIsExpired:
def test_none_expiry_never_expires(self):
assert is_expired(SimpleNamespace(expires_at=None)) is False
def test_a_future_expiry_is_live(self):
future = datetime.now(timezone.utc) + timedelta(hours=1)
assert is_expired(SimpleNamespace(expires_at=future)) is False
def test_a_past_expiry_is_expired(self):
past = datetime.now(timezone.utc) - timedelta(hours=1)
assert is_expired(SimpleNamespace(expires_at=past)) is True