test_printer_maintenance_unique_migration.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. """Duplicate maintenance rows are merged, and a unique index keeps them out (FO-C14).
  2. A printer's maintenance rows are created on first read, with no constraint:
  3. two dashboards, or a dashboard and the background check, could each create
  4. one. "Performed" could then update the copy that is not shown, and assigning
  5. the type again answered 500. The migration merges existing duplicates --
  6. keeping the most recently performed row and moving the others' history onto
  7. it -- and adds the index, which CREATE UNIQUE INDEX would refuse to build over
  8. duplicates.
  9. """
  10. from __future__ import annotations
  11. import pytest
  12. from sqlalchemy import text
  13. from sqlalchemy.exc import IntegrityError
  14. from sqlalchemy.ext.asyncio import create_async_engine
  15. from backend.app.core.database import _migrate_printer_maintenance_unique
  16. pytestmark = pytest.mark.asyncio
  17. @pytest.fixture
  18. async def engine():
  19. """In-memory SQLite with the two tables as they were before the index."""
  20. engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
  21. async with engine.begin() as conn:
  22. await conn.execute(
  23. text(
  24. "CREATE TABLE printer_maintenance ("
  25. "id INTEGER PRIMARY KEY AUTOINCREMENT, printer_id INTEGER NOT NULL, "
  26. "maintenance_type_id INTEGER NOT NULL, last_performed_at DATETIME, "
  27. "last_performed_hours FLOAT DEFAULT 0)"
  28. )
  29. )
  30. await conn.execute(
  31. text(
  32. "CREATE TABLE maintenance_history ("
  33. "id INTEGER PRIMARY KEY AUTOINCREMENT, printer_maintenance_id INTEGER NOT NULL)"
  34. )
  35. )
  36. try:
  37. yield engine
  38. finally:
  39. await engine.dispose()
  40. async def _item(conn, printer_id: int, type_id: int, performed_at: str | None = None) -> int:
  41. result = await conn.execute(
  42. text(
  43. "INSERT INTO printer_maintenance (printer_id, maintenance_type_id, last_performed_at) "
  44. "VALUES (:p, :t, :at) RETURNING id"
  45. ),
  46. {"p": printer_id, "t": type_id, "at": performed_at},
  47. )
  48. return result.scalar_one()
  49. async def _history(conn, item_id: int) -> None:
  50. await conn.execute(text("INSERT INTO maintenance_history (printer_maintenance_id) VALUES (:i)"), {"i": item_id})
  51. async def test_duplicates_merge_into_the_most_recently_performed_row(engine):
  52. async with engine.begin() as conn:
  53. never = await _item(conn, 1, 5)
  54. performed = await _item(conn, 1, 5, "2026-10-01 10:00:00")
  55. older = await _item(conn, 1, 5, "2026-09-01 10:00:00")
  56. other_type = await _item(conn, 1, 6)
  57. for item_id in (never, performed, older):
  58. await _history(conn, item_id)
  59. async with engine.begin() as conn:
  60. await _migrate_printer_maintenance_unique(conn)
  61. async with engine.begin() as conn:
  62. ids = (await conn.execute(text("SELECT id FROM printer_maintenance ORDER BY id"))).scalars().all()
  63. history = (await conn.execute(text("SELECT printer_maintenance_id FROM maintenance_history"))).scalars().all()
  64. assert ids == [performed, other_type]
  65. assert history == [performed, performed, performed]
  66. async def test_without_a_performance_the_row_the_page_showed_is_kept(engine):
  67. """The page took the last row read -- the highest id -- so a user's
  68. settings went there; keeping another would bring the defaults back."""
  69. async with engine.begin() as conn:
  70. await _item(conn, 1, 5)
  71. shown = await _item(conn, 1, 5)
  72. async with engine.begin() as conn:
  73. await _migrate_printer_maintenance_unique(conn)
  74. async with engine.begin() as conn:
  75. ids = (await conn.execute(text("SELECT id FROM printer_maintenance"))).scalars().all()
  76. assert ids == [shown]
  77. async def test_the_index_then_refuses_a_second_row(engine):
  78. async with engine.begin() as conn:
  79. await _migrate_printer_maintenance_unique(conn)
  80. await _item(conn, 1, 5)
  81. with pytest.raises(IntegrityError):
  82. async with engine.begin() as conn:
  83. await _item(conn, 1, 5)
  84. async def test_is_idempotent(engine):
  85. async with engine.begin() as conn:
  86. await _item(conn, 1, 5)
  87. await _item(conn, 2, 5)
  88. for _ in range(2):
  89. async with engine.begin() as conn:
  90. await _migrate_printer_maintenance_unique(conn)
  91. async with engine.begin() as conn:
  92. assert (await conn.execute(text("SELECT COUNT(*) FROM printer_maintenance"))).scalar_one() == 2