2309e9f43a
Five-service streaming platform: auth, catalogue, streaming, ingest, thumbnailer. Includes React frontend served by nginx, NATS JetStream event bus, aiobotocore async S3, PyAV video metadata + thumbnail extraction, service-to-service JWT auth, and a full unit + e2e test suite.
128 lines
3.9 KiB
Python
128 lines
3.9 KiB
Python
import subprocess
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
from streamstack.auth.app import app
|
|
from streamstack.auth.models import Base
|
|
from streamstack.core.db import get_db
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def rsa_key_pair(tmp_path_factory):
|
|
tmp = tmp_path_factory.mktemp("keys")
|
|
priv = tmp / "private.pem"
|
|
pub = tmp / "public.pem"
|
|
subprocess.run(
|
|
["openssl", "genrsa", "-out", str(priv), "2048"], check=True, capture_output=True
|
|
)
|
|
subprocess.run(
|
|
["openssl", "rsa", "-in", str(priv), "-pubout", "-out", str(pub)],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return str(priv), str(pub)
|
|
|
|
|
|
@pytest.fixture
|
|
async def db_session():
|
|
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with session_factory() as session:
|
|
yield session
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_nats():
|
|
nc = AsyncMock()
|
|
js = AsyncMock()
|
|
kv = AsyncMock()
|
|
nc.jetstream.return_value = js
|
|
js.key_value.return_value = kv
|
|
with patch("streamstack.auth.router.get_nats", return_value=nc):
|
|
yield nc
|
|
|
|
|
|
@pytest.fixture
|
|
async def test_client(db_session, mock_nats, rsa_key_pair):
|
|
priv, pub = rsa_key_pair
|
|
app.dependency_overrides[get_db] = lambda: db_session
|
|
with (
|
|
patch("streamstack.core.auth.settings") as ms,
|
|
patch("streamstack.auth.service.settings") as rs,
|
|
):
|
|
ms.jwt_private_key_path = priv
|
|
ms.jwt_public_key_path = pub
|
|
ms.jwt_algorithm = "RS256"
|
|
ms.jwt_expire_minutes = 30
|
|
ms.jwt_refresh_expire_days = 7
|
|
rs.jwt_public_key_path = pub
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
yield ac
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register(test_client):
|
|
resp = await test_client.post(
|
|
"/v1/auth/users/",
|
|
json={"email": "new@example.com", "password": "password123"},
|
|
)
|
|
assert resp.status_code == 201
|
|
body = resp.json()
|
|
assert body["email"] == "new@example.com"
|
|
assert "id" in body
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_duplicate_email(test_client):
|
|
creds = {"email": "dup@example.com", "password": "pass"}
|
|
await test_client.post("/v1/auth/users/", json=creds)
|
|
resp = await test_client.post("/v1/auth/users/", json=creds)
|
|
assert resp.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_success(test_client):
|
|
await test_client.post(
|
|
"/v1/auth/users/", json={"email": "login@example.com", "password": "correct"}
|
|
)
|
|
resp = await test_client.post(
|
|
"/v1/auth/token",
|
|
data={"username": "login@example.com", "password": "correct"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert "access_token" in body
|
|
assert "refresh_token" in body
|
|
assert body["token_type"] == "bearer"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_wrong_password(test_client):
|
|
await test_client.post(
|
|
"/v1/auth/users/", json={"email": "wp@example.com", "password": "correct"}
|
|
)
|
|
resp = await test_client.post(
|
|
"/v1/auth/token",
|
|
data={"username": "wp@example.com", "password": "wrong"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_unknown_user(test_client):
|
|
resp = await test_client.post(
|
|
"/v1/auth/token",
|
|
data={"username": "nobody@example.com", "password": "pass"},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
assert resp.status_code == 401
|