Face-Expression-Analyzer / backend /tests /api /test_session_isolation.py
Aynkhn's picture
Deploy FaceVision to Hugging Face Spaces
67af2df verified
Raw History Blame Contribute Delete
5.93 kB
"""Each browser session sees only its own records.
These are the tests that make the per-visitor dashboard trustworthy. Without
them, a scoping mistake in a query would silently expose every visitor's history
to every other visitor, and nothing else in the suite would notice.
"""
from __future__ import annotations
import pytest
BODY = {"dominant_emotion": "Happy", "duration_seconds": 10}
READ_ROUTES = ["/api/logs", "/api/logs/stats"]
def _post(client, headers, emotion="Happy"):
return client.post(
"/api/logs",
json={"dominant_emotion": emotion, "duration_seconds": 10},
headers=headers,
)
class TestSessionHeaderRequired:
@pytest.mark.parametrize("path", READ_ROUTES)
def test_reads_require_a_session_id(self, client, path):
assert client.get(path).status_code == 400
def test_writes_require_a_session_id(self, client):
response = client.post("/api/logs", json=BODY, headers={"X-API-Key": "test-ingest-key"})
assert response.status_code == 400
@pytest.mark.parametrize("bad", ["short", "has spaces here!", "x" * 65, ""])
def test_malformed_session_ids_are_rejected(self, client, bad):
response = client.get("/api/logs", headers={"X-Session-Id": bad})
assert response.status_code == 400
class TestReadIsolation:
def test_a_session_sees_only_its_own_entries(
self, client, ingest_headers, other_session_headers, session_headers
):
_post(client, ingest_headers, "Happy")
_post(client, ingest_headers, "Sad")
_post(client, other_session_headers, "Angry")
body = client.get("/api/logs", headers=session_headers).json()
assert body["total"] == 2
assert {item["dominant_emotion"] for item in body["items"]} == {"Happy", "Sad"}
def test_the_other_session_sees_only_its_own(
self, client, ingest_headers, other_session_headers
):
_post(client, ingest_headers, "Happy")
_post(client, other_session_headers, "Angry")
body = client.get(
"/api/logs", headers={"X-Session-Id": other_session_headers["X-Session-Id"]}
).json()
assert body["total"] == 1
assert body["items"][0]["dominant_emotion"] == "Angry"
def test_stats_are_scoped_too(
self, client, ingest_headers, other_session_headers, session_headers
):
for _ in range(3):
_post(client, ingest_headers, "Happy")
_post(client, other_session_headers, "Sad")
stats = client.get("/api/logs/stats", headers=session_headers).json()
assert stats["total_count"] == 3
assert {e["emotion"] for e in stats["by_emotion"]} == {"Happy"}
def test_an_empty_session_sees_nothing(self, client, ingest_headers):
_post(client, ingest_headers, "Happy")
body = client.get("/api/logs", headers={"X-Session-Id": "a-brand-new-session-99"}).json()
assert body["total"] == 0
class TestCrossSessionAccessIsDenied:
def test_cannot_fetch_another_sessions_entry_by_id(
self, client, ingest_headers, other_session_headers
):
created = _post(client, other_session_headers).json()
response = client.get(
f"/api/logs/{created['id']}",
headers={"X-Session-Id": ingest_headers["X-Session-Id"]},
)
# 404 rather than 403: an id you do not own is indistinguishable from one
# that does not exist, so the endpoint cannot be used to probe.
assert response.status_code == 404
def test_cannot_delete_another_sessions_entry(
self, client, ingest_headers, other_session_headers, session_headers
):
created = _post(client, other_session_headers).json()
response = client.delete(f"/api/logs/{created['id']}", headers=session_headers)
assert response.status_code == 404
# And it is still there for its owner.
owner = client.get(
"/api/logs", headers={"X-Session-Id": other_session_headers["X-Session-Id"]}
).json()
assert owner["total"] == 1
def test_clear_all_only_clears_the_callers_records(
self, client, ingest_headers, other_session_headers, session_headers
):
_post(client, ingest_headers, "Happy")
_post(client, ingest_headers, "Sad")
_post(client, other_session_headers, "Angry")
response = client.delete("/api/logs?confirm=true", headers=session_headers)
assert response.json()["deleted"] == 2
survivor = client.get(
"/api/logs", headers={"X-Session-Id": other_session_headers["X-Session-Id"]}
).json()
assert survivor["total"] == 1
class TestCorsAllowsTheSessionHeader:
def test_preflight_permits_x_session_id(self, client):
"""A header missing from allow_headers is blocked before it is sent.
curl ignores CORS, so this failure mode is invisible outside a browser:
every request simply never leaves the page.
"""
response = client.options(
"/api/logs",
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "x-session-id",
},
)
allowed = response.headers.get("access-control-allow-headers", "").lower()
assert "x-session-id" in allowed
class TestNoOperatorKeyRequired:
def test_reading_needs_no_api_key(self, client, ingest_headers, session_headers):
_post(client, ingest_headers)
# A plain session id is enough; there is no operator credential.
assert "X-API-Key" not in session_headers
assert client.get("/api/logs", headers=session_headers).status_code == 200
def test_response_does_not_echo_the_session_id(self, client, ingest_headers):
body = _post(client, ingest_headers).json()
assert "session_id" not in body