Files
bambuddy/backend/tests/integration/test_oidc_icon_deferred_load.py
Sn0rrii 8a7598f6b5 feat(auth): proxy OIDC provider icons server-side (#1333) (#1342)
* feat(auth): proxy OIDC provider icons server-side (#1333)

Strict img-src CSP blocked external OIDC icon hosts on the login page.
Loosening CSP was rejected via the MakerWorld precedent, so icons are
proxied: admin sets icon_url, backend fetches and caches the bytes in a
deferred BLOB column, the SPA renders from a same-origin
/api/v1/auth/oidc/providers/{id}/icon endpoint.
2026-05-15 08:50:37 +02:00

96 lines
3.5 KiB
Python

"""Verify icon_data stays deferred on list queries (#1333).
Regression guard: if ``deferred=True`` ever gets dropped from
``OIDCProvider.icon_data``, every login-page hit pulls the full BLOB on
the listing query, adding ~MB of bandwidth per anonymous request. These
tests assert via SQLAlchemy's instance inspector that the column is
**not** loaded by default and **is** loaded after an explicit
``undefer()``.
"""
import hashlib
import pytest
from sqlalchemy import inspect, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import undefer
_PNG_BYTES = bytes.fromhex(
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4"
"890000000d49444154789c63000100000005000100"
"0d0a2db40000000049454e44ae426082"
)
async def _seed_provider(db_session: AsyncSession, name: str = "DeferredProv"):
from backend.app.models.oidc_provider import OIDCProvider
provider = OIDCProvider(
name=name,
issuer_url="https://idp.example.com",
client_id="c",
scopes="openid",
is_enabled=True,
)
provider.client_secret = "secret"
provider.icon_url = "https://example.com/icon.png"
provider.icon_data = _PNG_BYTES
provider.icon_content_type = "image/png"
provider.icon_etag = hashlib.sha256(_PNG_BYTES).hexdigest()
db_session.add(provider)
await db_session.commit()
db_session.expire_all() # force the next read to come from DB, not identity-map
@pytest.mark.asyncio
@pytest.mark.integration
async def test_default_list_query_does_not_load_icon_data(db_session: AsyncSession):
"""`select(OIDCProvider)` without options keeps icon_data unloaded."""
from backend.app.models.oidc_provider import OIDCProvider
await _seed_provider(db_session)
result = await db_session.execute(select(OIDCProvider))
provider = result.scalar_one()
state = inspect(provider)
assert "icon_data" in state.unloaded, (
"icon_data should be deferred on the default list query — "
"without this guard every login page hit pulls the full BLOB."
)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_undefer_loads_icon_data(db_session: AsyncSession):
"""`select(...).options(undefer(...))` loads icon_data eagerly."""
from backend.app.models.oidc_provider import OIDCProvider
await _seed_provider(db_session, name="UndeferProv")
result = await db_session.execute(
select(OIDCProvider).options(undefer(OIDCProvider.icon_data)).where(OIDCProvider.name == "UndeferProv")
)
provider = result.scalar_one()
state = inspect(provider)
assert "icon_data" not in state.unloaded, "undefer() must eagerly load icon_data"
# And the bytes are accessible without raising MissingGreenlet.
assert provider.icon_data == _PNG_BYTES
@pytest.mark.asyncio
@pytest.mark.integration
async def test_icon_content_type_is_eager_indicator(db_session: AsyncSession):
"""icon_content_type must NOT be deferred — it's the eager has-icon
indicator that route handlers consult instead of icon_data, so it must
be loaded on every default query."""
from backend.app.models.oidc_provider import OIDCProvider
await _seed_provider(db_session, name="IndicatorProv")
result = await db_session.execute(select(OIDCProvider).where(OIDCProvider.name == "IndicatorProv"))
provider = result.scalar_one()
state = inspect(provider)
assert "icon_content_type" not in state.unloaded
# Direct access does not raise MissingGreenlet (it was already loaded).
assert provider.icon_content_type == "image/png"