media-rip/backend/tests/test_session_middleware.py
xpltd efc2ead796 M001: media.rip() v1.0 — complete application
Full-featured self-hosted yt-dlp web frontend:
- Python 3.12+ / FastAPI backend with async SQLite, SSE transport, session isolation
- Vue 3 / TypeScript / Pinia frontend with real-time progress, theme picker
- 3 built-in themes (cyberpunk/dark/light) + drop-in custom theme system
- Admin auth (bcrypt), purge system, cookie upload, file serving
- Docker multi-stage build, GitHub Actions CI/CD
- 179 backend tests, 29 frontend tests (208 total)

Slices: S01 (Foundation), S02 (SSE+Sessions), S03 (Frontend),
        S04 (Admin+Auth), S05 (Themes), S06 (Docker+CI)
2026-03-18 20:00:17 -05:00

190 lines
6 KiB
Python

"""Tests for the cookie-based SessionMiddleware."""
from __future__ import annotations
import asyncio
import uuid
import pytest
import pytest_asyncio
from fastapi import FastAPI, Request
from httpx import ASGITransport, AsyncClient
from app.core.config import AppConfig
from app.core.database import close_db, get_session, init_db
from app.middleware.session import SessionMiddleware
def _build_test_app(config, db_conn):
"""Build a minimal FastAPI app with SessionMiddleware and a probe endpoint."""
app = FastAPI()
app.add_middleware(SessionMiddleware)
app.state.config = config
app.state.db = db_conn
@app.get("/probe")
async def probe(request: Request):
return {"session_id": request.state.session_id}
return app
@pytest_asyncio.fixture()
async def mw_app(tmp_path):
"""Yield (app, db_conn, config) for middleware-focused tests."""
db_path = str(tmp_path / "session_mw.db")
config = AppConfig(server={"db_path": db_path})
db_conn = await init_db(db_path)
app = _build_test_app(config, db_conn)
yield app, db_conn, config
await close_db(db_conn)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_new_session_sets_cookie(mw_app):
"""Request without cookie → response has Set-Cookie with mrip_session, httpOnly, SameSite=Lax."""
app, db_conn, _ = mw_app
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
resp = await ac.get("/probe")
assert resp.status_code == 200
session_id = resp.json()["session_id"]
assert len(session_id) == 36 # UUID format
cookie_header = resp.headers.get("set-cookie", "")
assert f"mrip_session={session_id}" in cookie_header
assert "httponly" in cookie_header.lower()
assert "samesite=lax" in cookie_header.lower()
assert "path=/" in cookie_header.lower()
# Max-Age should be 72 * 3600 = 259200
assert "max-age=259200" in cookie_header.lower()
# Session should exist in DB
row = await get_session(db_conn, session_id)
assert row is not None
assert row["id"] == session_id
@pytest.mark.asyncio
async def test_reuse_valid_cookie(mw_app):
"""Request with valid mrip_session cookie → reuses session, last_seen updated."""
app, db_conn, _ = mw_app
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
# First request creates session
resp1 = await ac.get("/probe")
session_id = resp1.json()["session_id"]
# Read initial last_seen
row_before = await get_session(db_conn, session_id)
# Second request with cookie (httpx auto-sends it)
resp2 = await ac.get("/probe")
assert resp2.json()["session_id"] == session_id
# last_seen should be updated (or at least present)
row_after = await get_session(db_conn, session_id)
assert row_after is not None
assert row_after["last_seen"] >= row_before["last_seen"]
@pytest.mark.asyncio
async def test_invalid_cookie_creates_new_session(mw_app):
"""Request with invalid (non-UUID) cookie → new session created, new cookie set."""
app, db_conn, _ = mw_app
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
resp = await ac.get("/probe", cookies={"mrip_session": "not-a-uuid"})
assert resp.status_code == 200
session_id = resp.json()["session_id"]
assert session_id != "not-a-uuid"
assert len(session_id) == 36
# New session should exist in DB
row = await get_session(db_conn, session_id)
assert row is not None
# Cookie should be set with the new session
cookie_header = resp.headers.get("set-cookie", "")
assert f"mrip_session={session_id}" in cookie_header
@pytest.mark.asyncio
async def test_uuid_cookie_not_in_db_recreates(mw_app):
"""Request with valid UUID cookie not in DB → session created with that UUID."""
app, db_conn, _ = mw_app
transport = ASGITransport(app=app)
orphan_id = str(uuid.uuid4())
async with AsyncClient(transport=transport, base_url="http://test") as ac:
resp = await ac.get("/probe", cookies={"mrip_session": orphan_id})
assert resp.status_code == 200
# Should reuse the UUID from the cookie
assert resp.json()["session_id"] == orphan_id
# Session should now exist in DB
row = await get_session(db_conn, orphan_id)
assert row is not None
assert row["id"] == orphan_id
@pytest.mark.asyncio
async def test_open_mode_no_cookie(tmp_path):
"""Open mode → no cookie set, request.state.session_id == 'open'."""
db_path = str(tmp_path / "open_mode.db")
config = AppConfig(
server={"db_path": db_path},
session={"mode": "open"},
)
db_conn = await init_db(db_path)
app = _build_test_app(config, db_conn)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
resp = await ac.get("/probe")
await close_db(db_conn)
assert resp.status_code == 200
assert resp.json()["session_id"] == "open"
# No Set-Cookie header in open mode
cookie_header = resp.headers.get("set-cookie", "")
assert "mrip_session" not in cookie_header
@pytest.mark.asyncio
async def test_max_age_reflects_config(tmp_path):
"""Cookie Max-Age reflects config.session.timeout_hours."""
db_path = str(tmp_path / "maxage.db")
config = AppConfig(
server={"db_path": db_path},
session={"timeout_hours": 24},
)
db_conn = await init_db(db_path)
app = _build_test_app(config, db_conn)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
resp = await ac.get("/probe")
await close_db(db_conn)
cookie_header = resp.headers.get("set-cookie", "")
# 24 * 3600 = 86400
assert "max-age=86400" in cookie_header.lower()