- Extends GET /api/v1/tags with sort=count_desc and min_count query params - New TagsComponent at /tags (public, no auth guard) shows all tags sorted by image count - Clicking a tag navigates to /?tags=<name> for a pre-filtered library view - LibraryComponent reads ?tags= query param on init to support deep-linking from tag browser - Library header gains a "Browse tags" link to /tags for discoverability - All 15 TDD tasks complete; ruff, ng lint, and ng build clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
import os
|
|
|
|
import pytest_asyncio
|
|
from httpx import ASGITransport, AsyncClient
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
# Provide required settings for the test environment before any app imports resolve them
|
|
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-testing-only")
|
|
os.environ.setdefault("OWNER_USERNAME", "testowner")
|
|
os.environ.setdefault("OWNER_PASSWORD", "testpassword")
|
|
|
|
from app.auth.jwt_provider import JWTAuthProvider
|
|
from app.config import get_settings
|
|
from app.database import Base
|
|
from app.dependencies import get_auth, get_db, get_storage
|
|
from app.main import app
|
|
|
|
# Bust the LRU cache so get_settings() picks up the env vars set above
|
|
get_settings.cache_clear()
|
|
|
|
_TEST_JWT_SECRET = os.environ["JWT_SECRET_KEY"]
|
|
_TEST_OWNER_USERNAME = os.environ["OWNER_USERNAME"]
|
|
_TEST_OWNER_PASSWORD = os.environ["OWNER_PASSWORD"]
|
|
|
|
|
|
@pytest_asyncio.fixture(scope="session", loop_scope="session")
|
|
async def engine():
|
|
settings = get_settings()
|
|
# Use a separate test database URL if TEST_DATABASE_URL is set
|
|
import os
|
|
db_url = os.getenv("TEST_DATABASE_URL", settings.database_url)
|
|
eng = create_async_engine(db_url, echo=False)
|
|
async with eng.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
yield eng
|
|
async with eng.begin() as conn:
|
|
await conn.run_sync(Base.metadata.drop_all)
|
|
await eng.dispose()
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def db_session(engine):
|
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with session_factory() as session:
|
|
yield session
|
|
await session.rollback()
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def client(db_session):
|
|
from app.auth.noop import NoOpAuthProvider
|
|
from app.storage.s3_backend import S3StorageBackend
|
|
|
|
storage = S3StorageBackend()
|
|
auth = NoOpAuthProvider()
|
|
|
|
async def override_db():
|
|
yield db_session
|
|
|
|
def override_storage():
|
|
return storage
|
|
|
|
def override_auth():
|
|
return auth
|
|
|
|
app.dependency_overrides[get_db] = override_db
|
|
app.dependency_overrides[get_storage] = override_storage
|
|
app.dependency_overrides[get_auth] = override_auth
|
|
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
|
yield c
|
|
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def jwt_auth_provider() -> JWTAuthProvider:
|
|
return JWTAuthProvider(
|
|
secret_key=_TEST_JWT_SECRET,
|
|
expiry_seconds=3600,
|
|
owner_username=_TEST_OWNER_USERNAME,
|
|
owner_password=_TEST_OWNER_PASSWORD,
|
|
)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def authed_client(db_session, jwt_auth_provider):
|
|
from app.storage.s3_backend import S3StorageBackend
|
|
|
|
storage = S3StorageBackend()
|
|
auth = jwt_auth_provider
|
|
|
|
async def override_db():
|
|
yield db_session
|
|
|
|
def override_storage():
|
|
return storage
|
|
|
|
def override_auth():
|
|
return auth
|
|
|
|
app.dependency_overrides[get_db] = override_db
|
|
app.dependency_overrides[get_storage] = override_storage
|
|
app.dependency_overrides[get_auth] = override_auth
|
|
|
|
valid_token = auth.create_token()
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
|
yield c, valid_token
|
|
|
|
app.dependency_overrides.clear()
|