Initial Aurora Core 0.1 implementation
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s

Production-grade media metadata backend per docs/aurora-core-spec.md:
Aurora-owned UUIDv7 identity system, normalized movie/TV/people/credits
schema, provider abstraction with a TMDb adapter, an idempotent async
ingestion pipeline (normalize -> match -> upsert -> provenance), a
Postgres FTS/trigram search layer, a stateless FastAPI v1 API with
Redis caching/rate-limiting, arq workers, dev/prod Docker Compose
topologies, unit + integration + load tests, CI, and architecture/
database/API/ERD documentation.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-04 17:34:03 -05:00
co-authored by Claude Sonnet 5
commit c20df1c503
111 changed files with 10041 additions and 0 deletions
View File
+81
View File
@@ -0,0 +1,81 @@
"""Minimal data builders for integration tests — inserts ORM rows directly
so tests don't depend on network calls to a real provider."""
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.ids import aurora_id
from app.models.artwork import Artwork
from app.models.credit import Credit
from app.models.enums import ArtworkType, MovieStatus
from app.models.external_id import ExternalID
from app.models.genre import Genre, MovieGenre
from app.models.movie import Movie
from app.models.person import Person
from app.models.provider import Provider
async def make_provider(session: AsyncSession, slug: str = "tmdb") -> Provider:
provider = Provider(slug=slug, name=slug.upper())
session.add(provider)
await session.flush()
return provider
async def make_movie(
session: AsyncSession, *, title: str = "The Matrix", provider: Provider | None = None
) -> Movie:
movie = Movie(
title=title,
sort_title=title,
overview="A hacker discovers reality is a simulation.",
status=MovieStatus.RELEASED,
runtime_minutes=136,
)
session.add(movie)
await session.flush()
genre = Genre(name="Science Fiction", slug="science-fiction")
session.add(genre)
await session.flush()
session.add(MovieGenre(movie_id=movie.id, genre_id=genre.id))
person = Person(name="Keanu Reeves")
session.add(person)
await session.flush()
session.add(
Credit(
person_id=person.id,
entity_type="movie",
entity_id=movie.id,
department="Acting",
character="Neo",
billing_order=0,
)
)
session.add(
Artwork(
entity_type="movie",
entity_id=movie.id,
artwork_type=ArtworkType.POSTER,
provider_id=(provider or await make_provider(session)).id,
original_url="https://image.tmdb.org/t/p/original/poster.jpg",
is_primary=True,
)
)
if provider is not None:
session.add(
ExternalID(
entity_type="movie", entity_id=movie.id, provider_id=provider.id, external_id="603"
)
)
await session.flush()
return movie
def movie_aurora_id(movie: Movie) -> str:
return aurora_id("movie", str(movie.id))
+26
View File
@@ -0,0 +1,26 @@
"""Integration tests for liveness/readiness (section 24). Requires
PostgreSQL and Redis — see tests/conftest.py."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.usefixtures("db_session")
@pytest.mark.asyncio
async def test_health_is_always_ok(client: AsyncClient):
response = await client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.asyncio
async def test_ready_reports_postgresql_dependency(client: AsyncClient):
response = await client.get("/ready")
assert response.status_code == 200
body = response.json()
names = {dep["name"] for dep in body["dependencies"]}
assert "postgresql" in names
assert "redis" in names
+105
View File
@@ -0,0 +1,105 @@
"""Integration tests for the movie ingestion pipeline (section 15): a fake
provider stands in for TMDb so these run without network access, while
still exercising the real normalize -> match -> upsert -> provenance path
against PostgreSQL."""
from __future__ import annotations
from typing import Any
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.credit import Credit
from app.models.genre import Genre
from app.models.movie import Movie
from app.models.provider import ProviderRecord
from app.providers.base import MetadataProvider
from app.services.ingestion.movie_ingestion import ingest_movie
from tests.unit.test_normalizer import FAKE_MOVIE
class FakeTMDbProvider(MetadataProvider):
slug = "tmdb"
name = "The Movie Database (TMDb)"
def __init__(self, payload: dict[str, Any]) -> None:
self._payload = payload
async def get_movie(self, external_id: str) -> dict[str, Any]:
return self._payload
async def get_series(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def get_season(self, series_external_id: str, season_number: int) -> dict[str, Any]:
raise NotImplementedError
async def get_person(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def search_movie(self, query: str) -> list[dict[str, Any]]:
raise NotImplementedError
@pytest.mark.asyncio
async def test_ingest_movie_creates_normalized_record(db_session: AsyncSession):
provider = FakeTMDbProvider(FAKE_MOVIE)
movie_id = await ingest_movie(db_session, provider, "603")
await db_session.commit()
movie = await db_session.get(Movie, movie_id)
assert movie is not None
assert movie.title == "The Matrix"
assert movie.runtime_minutes == 136
genres = (await db_session.execute(select(Genre.name))).scalars().all()
assert "Action" in genres
credits_ = (
(await db_session.execute(select(Credit).where(Credit.entity_id == movie_id)))
.scalars()
.all()
)
assert any(c.character == "Neo" for c in credits_)
record = (
await db_session.execute(
select(ProviderRecord).where(ProviderRecord.external_id == "603")
)
).scalar_one()
assert record.aurora_entity_id == movie_id
assert record.match_confidence.value == "new"
@pytest.mark.asyncio
async def test_ingest_movie_is_idempotent(db_session: AsyncSession):
"""Re-running ingestion for the same provider + external_id must update
the same Aurora movie, never create a duplicate (section 2: zero
duplicate ingestion records)."""
provider = FakeTMDbProvider(FAKE_MOVIE)
first_id = await ingest_movie(db_session, provider, "603")
await db_session.commit()
updated_payload = {**FAKE_MOVIE, "title": "The Matrix (Updated Title)"}
second_provider = FakeTMDbProvider(updated_payload)
second_id = await ingest_movie(db_session, second_provider, "603")
await db_session.commit()
assert first_id == second_id
all_movies = (await db_session.execute(select(Movie))).scalars().all()
assert len(all_movies) == 1
assert all_movies[0].title == "The Matrix (Updated Title)"
credits_ = (
(await db_session.execute(select(Credit).where(Credit.entity_id == first_id)))
.scalars()
.all()
)
# Credits are replaced wholesale on re-sync, not duplicated.
assert len(credits_) == 2 # 1 cast + 1 crew from FAKE_MOVIE
+91
View File
@@ -0,0 +1,91 @@
"""Integration tests for the Movie API (section 17/18). Requires
PostgreSQL and Redis — see tests/conftest.py."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from tests.integration.factories import make_movie, make_provider, movie_aurora_id
@pytest.mark.asyncio
async def test_get_movie_returns_normalized_record(client: AsyncClient, db_session: AsyncSession):
provider = await make_provider(db_session)
movie = await make_movie(db_session, provider=provider)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}")
assert response.status_code == 200
body = response.json()
assert body["id"] == movie_aurora_id(movie)
assert body["type"] == "movie"
assert body["title"] == "The Matrix"
assert body["runtime"] == 136
assert {g["name"] for g in body["genres"]} == {"Science Fiction"}
assert len(body["cast"]) == 1
assert body["cast"][0]["character"] == "Neo"
assert body["images"]["posters"][0]["is_primary"] is True
assert body["external_ids"]["tmdb"] == "603"
@pytest.mark.asyncio
async def test_get_movie_404_for_unknown_id(client: AsyncClient):
from app.core.ids import aurora_id, new_uuid7
unknown_id = aurora_id("movie", new_uuid7())
response = await client.get(f"/api/v1/movies/{unknown_id}")
assert response.status_code == 404
assert response.json()["error"]["code"] == "movie_not_found"
@pytest.mark.asyncio
async def test_get_movie_rejects_malformed_id(client: AsyncClient):
response = await client.get("/api/v1/movies/not-a-real-id")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_get_movie_rejects_wrong_entity_type_id(
client: AsyncClient, db_session: AsyncSession
):
# A well-formed person ID must not resolve as a movie.
from app.core.ids import aurora_id, new_uuid7
person_shaped_id = aurora_id("person", new_uuid7())
response = await client.get(f"/api/v1/movies/{person_shaped_id}")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_get_movie_credits(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}/credits")
assert response.status_code == 200
body = response.json()
assert len(body["cast"]) == 1
assert body["cast"][0]["person"]["name"] == "Keanu Reeves"
@pytest.mark.asyncio
async def test_get_movie_images(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}/images")
assert response.status_code == 200
assert len(response.json()["images"]["posters"]) == 1
@pytest.mark.asyncio
async def test_get_movie_videos_empty(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}/videos")
assert response.status_code == 200
assert response.json()["videos"] == []