test_oidc_icon_migration_pg.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """Verify the dialect-conditional branch of the icon-column migration (#1333).
  2. ``run_migrations`` issues ``ALTER TABLE … ADD COLUMN icon_data {BLOB|BYTEA}``
  3. based on ``is_sqlite()``. The full migration only runs against a live
  4. engine, so we monkey-patch ``is_sqlite()`` and capture the SQL passed to
  5. ``_safe_execute``. Mirrors the test pattern at
  6. ``backend/tests/unit/test_db_dialect.py`` (lines around 539-606) which is
  7. already used to verify other dialect-conditional migrations.
  8. Without this test the PostgreSQL branch would be dead code in CI (the
  9. project's tests run on SQLite) and a typo in the BYTEA emission would
  10. slip silently to production, where ``_safe_execute`` would swallow the
  11. column-creation failure and PG users would never cache icon bytes.
  12. """
  13. from unittest.mock import AsyncMock, MagicMock, patch
  14. import pytest
  15. from backend.app.core import database as db_module
  16. class _AsyncCtxStub:
  17. """Async context manager that does nothing — for ``begin_nested()``."""
  18. async def __aenter__(self):
  19. return self
  20. async def __aexit__(self, *_exc):
  21. return False
  22. async def _capture_sql(is_sqlite_value: bool) -> list[str]:
  23. """Patch ``is_sqlite()`` + ``_safe_execute`` and return every SQL string
  24. that would have been executed during ``run_migrations``.
  25. Sub-migration callables that don't emit ALTER TABLE icon_data (the auto-
  26. link constraint update and the AMS-id widening) are no-op'd to keep the
  27. test focused on the icon migration.
  28. ``run_migrations`` uses ``async with conn.begin_nested()`` for the few
  29. DML backfills, so the fake conn returns a real async context manager.
  30. Inline ``conn.execute()`` calls (in the SQLite-recreation branch only,
  31. which we exclude) are also wired up to record SQL — but the bulk of
  32. the DDL goes through ``_safe_execute`` which is what we capture.
  33. """
  34. executed_sql: list[str] = []
  35. async def fake_safe_execute(_conn, sql: str) -> None:
  36. executed_sql.append(sql)
  37. fake_conn = MagicMock()
  38. fake_conn.begin_nested = lambda: _AsyncCtxStub()
  39. fake_conn.execute = AsyncMock(return_value=MagicMock(fetchone=MagicMock(return_value=None)))
  40. with (
  41. patch("backend.app.core.database.is_sqlite", return_value=is_sqlite_value),
  42. patch("backend.app.core.database._safe_execute", side_effect=fake_safe_execute),
  43. patch("backend.app.core.database._migrate_update_auto_link_constraint", AsyncMock()),
  44. patch("backend.app.core.database._migrate_widen_spoolman_slot_ams_id_range", AsyncMock()),
  45. ):
  46. await db_module.run_migrations(fake_conn)
  47. return executed_sql
  48. @pytest.mark.asyncio
  49. async def test_pg_branch_uses_bytea_for_icon_data():
  50. """is_sqlite()=False must emit ``ADD COLUMN icon_data BYTEA``."""
  51. executed = await _capture_sql(is_sqlite_value=False)
  52. icon_data_stmts = [s for s in executed if "ADD COLUMN icon_data" in s]
  53. assert len(icon_data_stmts) == 1, f"expected exactly one icon_data ADD COLUMN statement, got: {icon_data_stmts!r}"
  54. assert "BYTEA" in icon_data_stmts[0]
  55. assert "BLOB" not in icon_data_stmts[0]
  56. @pytest.mark.asyncio
  57. async def test_sqlite_branch_uses_blob_for_icon_data():
  58. """is_sqlite()=True must emit ``ADD COLUMN icon_data BLOB``.
  59. Companion to the PG test — together they guarantee the
  60. ``is_sqlite()`` switch wasn't accidentally inverted.
  61. """
  62. executed = await _capture_sql(is_sqlite_value=True)
  63. icon_data_stmts = [s for s in executed if "ADD COLUMN icon_data" in s]
  64. assert len(icon_data_stmts) == 1
  65. assert "BLOB" in icon_data_stmts[0]
  66. assert "BYTEA" not in icon_data_stmts[0]
  67. @pytest.mark.asyncio
  68. async def test_icon_content_type_and_etag_columns_both_dialects():
  69. """The two String columns are dialect-independent (VARCHAR works on
  70. both SQLite and PostgreSQL). Verify both branches emit them."""
  71. for is_sqlite_value in (True, False):
  72. executed = await _capture_sql(is_sqlite_value=is_sqlite_value)
  73. content_type_stmts = [s for s in executed if "ADD COLUMN icon_content_type" in s]
  74. etag_stmts = [s for s in executed if "ADD COLUMN icon_etag" in s]
  75. assert len(content_type_stmts) == 1, f"is_sqlite={is_sqlite_value}: {content_type_stmts!r}"
  76. assert len(etag_stmts) == 1, f"is_sqlite={is_sqlite_value}: {etag_stmts!r}"
  77. assert "VARCHAR" in content_type_stmts[0].upper()
  78. assert "VARCHAR" in etag_stmts[0].upper()