getting started with backend and uploader
This commit is contained in:
commit
1e1000268a
50 files changed
+2237
No files matched your search
Whitespace-only changes.
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in new issue
Block a user