test_vp_mode_rename_migration.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. """Regression test for the VP mode wire-value rename migration (#1429 follow-up).
  2. The UI buttons "Archive" and "Queue" had always saved the wire values
  3. `immediate` and `print_queue` — confusing in every support bundle. The
  4. rename migration in ``run_migrations`` rewrites existing rows to the
  5. canonical names. This test verifies it on both fresh and legacy schemas
  6. and confirms it's idempotent so reruns are safe (boot-on-boot).
  7. """
  8. from __future__ import annotations
  9. import pytest
  10. from sqlalchemy import text
  11. from sqlalchemy.ext.asyncio import create_async_engine
  12. from backend.app.core.database import run_migrations
  13. @pytest.fixture(autouse=True)
  14. def force_sqlite_dialect(monkeypatch):
  15. """Force the SQLite branch regardless of test env settings."""
  16. from backend.app.core import db_dialect
  17. monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
  18. monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
  19. from backend.app.core import database as database_module
  20. monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
  21. def _register_all_models():
  22. """run_migrations touches multiple tables; the full schema must exist."""
  23. from backend.app.models import ( # noqa: F401
  24. ams_history,
  25. ams_label,
  26. api_key,
  27. archive,
  28. color_catalog,
  29. external_link,
  30. filament,
  31. group,
  32. kprofile_note,
  33. maintenance,
  34. notification,
  35. notification_template,
  36. print_log,
  37. print_queue,
  38. printer,
  39. project,
  40. project_bom,
  41. settings,
  42. slot_preset,
  43. smart_plug,
  44. smart_plug_energy_snapshot,
  45. spool,
  46. spool_assignment,
  47. spool_catalog,
  48. spool_k_profile,
  49. spool_usage_history,
  50. spoolbuddy_device,
  51. user,
  52. user_email_pref,
  53. virtual_printer,
  54. )
  55. @pytest.fixture
  56. async def engine():
  57. from backend.app.core.database import Base
  58. _register_all_models()
  59. eng = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
  60. async with eng.begin() as conn:
  61. await conn.run_sync(Base.metadata.create_all)
  62. yield eng
  63. await eng.dispose()
  64. @pytest.mark.asyncio
  65. async def test_legacy_mode_rows_get_canonical_names(engine):
  66. """Existing rows with `immediate` / `print_queue` get rewritten to
  67. `archive` / `queue` while canonical values and unrelated modes pass
  68. through untouched."""
  69. async with engine.begin() as conn:
  70. await conn.execute(
  71. text(
  72. "INSERT INTO virtual_printers (id, name, enabled, mode, serial_suffix, position) VALUES "
  73. "(1, 'A', 0, 'immediate', '391800001', 1),"
  74. "(2, 'B', 0, 'print_queue', '391800002', 2),"
  75. "(3, 'C', 0, 'review', '391800003', 3),"
  76. "(4, 'D', 0, 'proxy', '391800004', 4),"
  77. "(5, 'E', 0, 'archive', '391800005', 5),"
  78. "(6, 'F', 0, 'queue', '391800006', 6)"
  79. )
  80. )
  81. async with engine.begin() as conn:
  82. await run_migrations(conn)
  83. async with engine.connect() as conn:
  84. result = await conn.execute(text("SELECT id, mode FROM virtual_printers ORDER BY id"))
  85. rows = dict(result.fetchall())
  86. assert rows[1] == "archive" # immediate → archive
  87. assert rows[2] == "queue" # print_queue → queue
  88. assert rows[3] == "review" # untouched
  89. assert rows[4] == "proxy" # untouched
  90. assert rows[5] == "archive" # already canonical
  91. assert rows[6] == "queue" # already canonical
  92. @pytest.mark.asyncio
  93. async def test_legacy_settings_row_gets_canonical_name(engine):
  94. """The legacy single-VP `virtual_printer_mode` setting also gets renamed
  95. so the GET response (which feeds the support bundle and the settings
  96. page) reads the canonical name."""
  97. async with engine.begin() as conn:
  98. await conn.execute(text("INSERT INTO settings (key, value) VALUES ('virtual_printer_mode', 'immediate')"))
  99. async with engine.begin() as conn:
  100. await run_migrations(conn)
  101. async with engine.connect() as conn:
  102. result = await conn.execute(text("SELECT value FROM settings WHERE key = 'virtual_printer_mode'"))
  103. value = result.scalar()
  104. assert value == "archive"
  105. @pytest.mark.asyncio
  106. async def test_migration_is_idempotent(engine):
  107. """Running the migration twice must be a no-op on canonical values —
  108. every boot re-runs the migration set."""
  109. async with engine.begin() as conn:
  110. await conn.execute(
  111. text(
  112. "INSERT INTO virtual_printers (id, name, enabled, mode, serial_suffix, position) "
  113. "VALUES (1, 'A', 0, 'immediate', '391800001', 1)"
  114. )
  115. )
  116. async with engine.begin() as conn:
  117. await run_migrations(conn)
  118. # Second run on already-canonical values.
  119. async with engine.begin() as conn:
  120. await run_migrations(conn)
  121. async with engine.connect() as conn:
  122. result = await conn.execute(text("SELECT mode FROM virtual_printers WHERE id = 1"))
  123. assert result.scalar() == "archive"