getting started with backend and uploader

This commit is contained in:
dreamer committed 2026-10-13 16:54:05 +02:00
commit 1e1000268a
50 files changed
+2237

No files matched your search

View File
Whitespace-only changes.
+52
View File
@@ -0,0 +1,52 @@
import asyncio
from collections.abc import AsyncGenerator, Iterator
import pytest
import pytest_asyncio
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from fdhview.api.deps import get_session
from fdhview.main import app
test_engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
test_session_maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
@pytest.fixture(autouse=True)
def setup_test_db() -> Iterator[None]:
"""Creates tables before each test and drops them after."""
async def _setup():
async with test_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
async def _teardown():
async with test_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.drop_all)
asyncio.run(_setup())
yield
asyncio.run(_teardown())
@pytest.fixture
def client() -> Iterator[TestClient]:
"""Test client that uses the in-memory database."""
async def override_get_session():
async with test_session_maker() as session:
yield session
app.dependency_overrides[get_session] = override_get_session
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
@pytest_asyncio.fixture
async def db_session() -> AsyncGenerator[AsyncSession]:
"""Async session for tests that need to seed or query data directly."""
async with test_session_maker() as session:
yield session
+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<AAMPos xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="pos_schema.xsd">
<sessions>
<session>
<id>12065371</id>
<screen_identifier>DenHaag_zaal_3</screen_identifier>
<complex_identifier>Filmhuis Den Haag</complex_identifier>
<start>202608201400</start>
<end>202608201534</end>
<feature_duration>5280</feature_duration>
<title>Mulan - EPIC SUMMER</title>
<rating>0</rating>
<seats_available>40</seats_available>
<seats_sold>fourteen</seats_sold>
<session_attributes>
<session_attribute>2D</session_attribute>
<session_attribute>Angst</session_attribute>
<session_attribute>Animation</session_attribute>
<session_attribute>Geweld</session_attribute>
<session_attribute>vanaf 6 jaar</session_attribute>
</session_attributes>
</session>
<session>
<id>12065374</id>
<screen_identifier>DenHaag_zaal_1</screen_identifier>
<complex_identifier>Filmhuis Den Haag</complex_identifier>
<start>202608212030</start>
<end>202608212339</end>
<feature_duration>10980</feature_duration>
<title>Apocalypse Now: The Final Cut - EPIC SUMMER</title>
<rating>0</rating>
<seats_available>98</seats_available>
<seats_sold>56</seats_sold>
<session_attributes>
<session_attribute>2D</session_attribute>
<session_attribute>Geweld</session_attribute>
<session_attribute>Grof taalgebruik</session_attribute>
<session_attribute>vanaf 16 jaar</session_attribute>
<session_attribute>War</session_attribute>
</session_attributes>
</session>
<session>
<id>12065375</id>
<screen_identifier>DenHaag_zaal_3</screen_identifier>
<complex_identifier>Filmhuis Den Haag</complex_identifier>
<start>202608211815</start>
<end>202608211957</end>
<feature_duration>5760</feature_duration>
<title>Hearts of Darkness: A Filmmaker's Apocalypse - EPIC SUMMER</title>
<rating>0</rating>
<seats_available>40</seats_available>
<seats_sold>23</seats_sold>
<session_attributes>
<session_attribute>2D</session_attribute>
<session_attribute>Documentary</session_attribute>
</session_attributes>
</session>
</sessions>
</AAMPos>
+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<AAMPos xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="pos_schema.xsd">
<sessions>
<session>
<id>12065371</id>
<screen_identifier>DenHaag_zaal_3</screen_identifier>
<complex_identifier>Filmhuis Den Haag</complex_identifier>
<start>202608201400</start>
<end>202608201534</end>
<feature_duration>5280</feature_duration>
<title>Mulan - EPIC SUMMER</title>
<rating>0</rating>
<seats_available>40</seats_available>
<seats_sold>14</seats_sold>
<session_attributes>
<session_attribute>2D</session_attribute>
<session_attribute>Angst</session_attribute>
<session_attribute>Animation</session_attribute>
<session_attribute>Geweld</session_attribute>
<session_attribute>vanaf 6 jaar</session_attribute>
</session_attributes>
</session>
<session>
<id>12065374</id>
<screen_identifier>DenHaag_zaal_1</screen_identifier>
<complex_identifier>Filmhuis Den Haag</complex_identifier>
<start>202608212030</start>
<end>202608212339</end>
<feature_duration>10980</feature_duration>
<title>Apocalypse Now: The Final Cut - EPIC SUMMER</title>
<rating>0</rating>
<seats_available>98</seats_available>
<seats_sold>56</seats_sold>
<session_attributes>
<session_attribute>2D</session_attribute>
<session_attribute>Geweld</session_attribute>
<session_attribute>Grof taalgebruik</session_attribute>
<session_attribute>vanaf 16 jaar</session_attribute>
<session_attribute>War</session_attribute>
</session_attributes>
</session>
<session>
<id>12065375</id>
<screen_identifier>DenHaag_zaal_3</screen_identifier>
<complex_identifier>Filmhuis Den Haag</complex_identifier>
<start>202608211815</start>
<end>202608211957</end>
<feature_duration>5760</feature_duration>
<title>Hearts of Darkness: A Filmmaker's Apocalypse - EPIC SUMMER</title>
<rating>0</rating>
<seats_available>40</seats_available>
<seats_sold>23</seats_sold>
<session_attributes>
<session_attribute>2D</session_attribute>
<session_attribute>Documentary</session_attribute>
</session_attributes>
</session>
</sessions>
</AAMPos>
+38
View File
@@ -0,0 +1,38 @@
from pathlib import Path
from fastapi.testclient import TestClient
from fdhview.core.config import settings
AUTH = ("username", settings.api_password)
def test_upload_requires_auth(client: TestClient):
response = client.post("/admin/upload")
assert response.status_code == 401
def test_upload_requests_wrong_ext(client: TestClient):
response = client.post(
"/admin/upload",
auth=AUTH,
files={"file": ("test.txt", b"not xml", "text/plain")})
assert response.status_code == 422
def test_upload_requests_bad_xml(client: TestClient):
response = client.post(
"/admin/upload",
auth=AUTH,
files={"file": ("test.xml", b"not xml", "application/xml")})
assert response.status_code == 422
def test_upload_succeeds_with_valid_xml(client: TestClient):
minimal_xml = Path("tests/data/minimal.xml").read_bytes()
response = client.post(
"/admin/upload",
auth=AUTH,
files={"file": ("minimal.xml", minimal_xml, "application/xml")})
assert response.status_code == 200
assert response.json()['filename'] == "minimal.xml"
+49
View File
@@ -0,0 +1,49 @@
from datetime import datetime
from pathlib import Path
import pytest
from fdhview.services.screenwriter.importer import _clean_title
from fdhview.services.screenwriter.schema import AAMPos
def test_xml():
xml = Path('tests/data/minimal.xml').read_text()
data = AAMPos.from_xml(xml)
sessions = data.sessions.root
assert isinstance(sessions[1].start, datetime)
assert isinstance(sessions[1].end, datetime)
assert len(sessions[1].attributes) == 5
assert len(sessions[2].attributes) == 2
def test_broken_xml():
xml = Path('tests/data/broken.xml').read_text()
with pytest.raises(ValueError):
AAMPos.from_xml(xml)
def test_clean_title():
# Basic cases
assert _clean_title("Good Movie - EN subs") == "Good Movie"
assert _clean_title("Good Movie- EN subs") == "Good Movie"
assert _clean_title("Good Movie - en subs") == "Good Movie"
assert _clean_title("Good Movie - NL sub") == "Good Movie"
# Presentation / talk variations
assert _clean_title("Good Movie - met inleiding") == "Good Movie"
assert _clean_title("Good Movie - met inleiding en nagesprek") == "Good Movie"
assert _clean_title("Good Movie – met nagesprek") == "Good Movie"
assert _clean_title("Good Movie - met voorfilms en nagesprek") == "Good Movie"
assert _clean_title("Good Movie - EN subs met inleiding") == "Good Movie"
# Stacked suffixes
assert _clean_title("Good Movie - met inleiding - EN subs") == "Good Movie"
# Titles that should NOT be modified
assert _clean_title("Good Movie") == "Good Movie"
assert _clean_title("De la Comédie-Française") == "De la Comédie-Française"
assert _clean_title("Dikkie Dik (re-release)") == "Dikkie Dik (re-release)"
assert _clean_title("Aguirre - Epic Summer - EN subs") == "Aguirre - Epic Summer"
+53
View File
@@ -0,0 +1,53 @@
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import pytest
from sqlmodel.ext.asyncio.session import AsyncSession
from fdhview.core.config import settings
from fdhview.models.enums import Screen
from fdhview.models.screening import Screening
@pytest.mark.asyncio
async def test_infodesk_filters_past(
client, db_session: AsyncSession
):
now = datetime.now(tz=ZoneInfo(settings.timezone))
# Past film (ended 15 mins ago)
past = Screening(
session=1, screen=Screen.ONE, complex_ident="FDH",
start=now - timedelta(hours=2), end=now - timedelta(minutes=15),
duration=105, title="Past Film", rating=0, seats_total=100, seats_sold=10
)
# Currently running film (started 30 mins ago, ends in 60 mins)
running = Screening(
session=2, screen=Screen.TWO, complex_ident="FDH",
start=now - timedelta(minutes=30), end=now + timedelta(minutes=60),
duration=90, title="Running Film", rating=0, seats_total=100, seats_sold=50
)
# Upcoming film (starts in 2 hours)
upcoming = Screening(
session=3, screen=Screen.THREE, complex_ident="FDH",
start=now + timedelta(hours=2), end=now + timedelta(hours=4),
duration=120, title="Upcoming Film", rating=0, seats_total=100, seats_sold=20
)
db_session.add_all([past, running, upcoming])
await db_session.commit()
response = client.get("/api/screenings")
assert response.status_code == 200
titles = [s["title"] for s in response.json()]
assert "Past Film" not in titles
assert "Running Film" not in titles
assert "Upcoming Film" in titles
@pytest.mark.asyncio
async def test_get_screening_prices(client):
response = client.get("/api/screenings/prices")
assert response.status_code == 200
assert response.json() == settings.prices