test_vp_mode_rename_migration.py 5.2 KB

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