test_oidc_icon_deferred_load.py 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """Verify icon_data stays deferred on list queries (#1333).
  2. Regression guard: if ``deferred=True`` ever gets dropped from
  3. ``OIDCProvider.icon_data``, every login-page hit pulls the full BLOB on
  4. the listing query, adding ~MB of bandwidth per anonymous request. These
  5. tests assert via SQLAlchemy's instance inspector that the column is
  6. **not** loaded by default and **is** loaded after an explicit
  7. ``undefer()``.
  8. """
  9. import hashlib
  10. import pytest
  11. from sqlalchemy import inspect, select
  12. from sqlalchemy.ext.asyncio import AsyncSession
  13. from sqlalchemy.orm import undefer
  14. _PNG_BYTES = bytes.fromhex(
  15. "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4"
  16. "890000000d49444154789c63000100000005000100"
  17. "0d0a2db40000000049454e44ae426082"
  18. )
  19. async def _seed_provider(db_session: AsyncSession, name: str = "DeferredProv"):
  20. from backend.app.models.oidc_provider import OIDCProvider
  21. provider = OIDCProvider(
  22. name=name,
  23. issuer_url="https://idp.example.com",
  24. client_id="c",
  25. scopes="openid",
  26. is_enabled=True,
  27. )
  28. provider.client_secret = "secret"
  29. provider.icon_url = "https://example.com/icon.png"
  30. provider.icon_data = _PNG_BYTES
  31. provider.icon_content_type = "image/png"
  32. provider.icon_etag = hashlib.sha256(_PNG_BYTES).hexdigest()
  33. db_session.add(provider)
  34. await db_session.commit()
  35. db_session.expire_all() # force the next read to come from DB, not identity-map
  36. @pytest.mark.asyncio
  37. @pytest.mark.integration
  38. async def test_default_list_query_does_not_load_icon_data(db_session: AsyncSession):
  39. """`select(OIDCProvider)` without options keeps icon_data unloaded."""
  40. from backend.app.models.oidc_provider import OIDCProvider
  41. await _seed_provider(db_session)
  42. result = await db_session.execute(select(OIDCProvider))
  43. provider = result.scalar_one()
  44. state = inspect(provider)
  45. assert "icon_data" in state.unloaded, (
  46. "icon_data should be deferred on the default list query — "
  47. "without this guard every login page hit pulls the full BLOB."
  48. )
  49. @pytest.mark.asyncio
  50. @pytest.mark.integration
  51. async def test_undefer_loads_icon_data(db_session: AsyncSession):
  52. """`select(...).options(undefer(...))` loads icon_data eagerly."""
  53. from backend.app.models.oidc_provider import OIDCProvider
  54. await _seed_provider(db_session, name="UndeferProv")
  55. result = await db_session.execute(
  56. select(OIDCProvider).options(undefer(OIDCProvider.icon_data)).where(OIDCProvider.name == "UndeferProv")
  57. )
  58. provider = result.scalar_one()
  59. state = inspect(provider)
  60. assert "icon_data" not in state.unloaded, "undefer() must eagerly load icon_data"
  61. # And the bytes are accessible without raising MissingGreenlet.
  62. assert provider.icon_data == _PNG_BYTES
  63. @pytest.mark.asyncio
  64. @pytest.mark.integration
  65. async def test_icon_content_type_is_eager_indicator(db_session: AsyncSession):
  66. """icon_content_type must NOT be deferred — it's the eager has-icon
  67. indicator that route handlers consult instead of icon_data, so it must
  68. be loaded on every default query."""
  69. from backend.app.models.oidc_provider import OIDCProvider
  70. await _seed_provider(db_session, name="IndicatorProv")
  71. result = await db_session.execute(select(OIDCProvider).where(OIDCProvider.name == "IndicatorProv"))
  72. provider = result.scalar_one()
  73. state = inspect(provider)
  74. assert "icon_content_type" not in state.unloaded
  75. # Direct access does not raise MissingGreenlet (it was already loaded).
  76. assert provider.icon_content_type == "image/png"