test_github_restore_numbers.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """Numbers restored from a GitHub backup are checked like numbers typed in.
  2. Archive and spool-usage restore copied grams, costs, energy and print time
  3. straight from the backup: a hand-edited or damaged backup could put negative or
  4. absurd values (or text) into the filament and cost totals, which the archive
  5. editor itself refuses. Each such field now has to be a finite number of at
  6. least 0 (grams at most 100000, as the editor allows); anything else is left
  7. empty, said once per category.
  8. """
  9. import math
  10. import pytest
  11. from sqlalchemy import select
  12. from backend.app.models.archive import PrintArchive
  13. from backend.app.models.spool_usage_history import SpoolUsageHistory
  14. from backend.app.services.github_restore import _CategoryTally
  15. from backend.tests.unit.test_github_restore import TestRestoreArchives, _codes, _service
  16. pytestmark = pytest.mark.asyncio
  17. def _entry(**numbers):
  18. return TestRestoreArchives()._archive_entry(**numbers)
  19. async def _restore_archive(db_session, entry):
  20. tally = _CategoryTally()
  21. await _service()._restore_archives(db_session, {"archives": [entry]}, False, tally, {})
  22. await db_session.commit()
  23. return (await db_session.execute(select(PrintArchive))).scalar_one(), tally
  24. async def test_good_numbers_come_through(db_session):
  25. row, tally = await _restore_archive(
  26. db_session,
  27. _entry(
  28. filament_used_grams=45.2,
  29. cost=1.13,
  30. energy_kwh=0.4,
  31. energy_cost=0.12,
  32. wear_cost=0.05,
  33. print_time_seconds=3600,
  34. ),
  35. )
  36. assert (
  37. row.filament_used_grams,
  38. row.cost,
  39. row.energy_kwh,
  40. row.energy_cost,
  41. row.wear_cost,
  42. row.print_time_seconds,
  43. ) == (
  44. 45.2,
  45. 1.13,
  46. 0.4,
  47. 0.12,
  48. 0.05,
  49. 3600,
  50. )
  51. assert "archivesNumbersDropped" not in _codes(tally)
  52. @pytest.mark.parametrize(
  53. "bad",
  54. [
  55. {"filament_used_grams": -5},
  56. {"filament_used_grams": 1e9},
  57. {"cost": "12"},
  58. {"energy_kwh": math.inf},
  59. {"energy_cost": float("nan")},
  60. {"wear_cost": True},
  61. {"print_time_seconds": -1},
  62. ],
  63. ids=["negative-grams", "absurd-grams", "text-cost", "infinite-energy", "nan-cost", "bool-wear", "negative-time"],
  64. )
  65. async def test_a_bad_number_is_left_empty_and_said_once(db_session, bad):
  66. row, tally = await _restore_archive(db_session, _entry(**bad))
  67. (field,) = bad
  68. assert getattr(row, field) is None
  69. assert _codes(tally).count("archivesNumbersDropped") == 1
  70. async def test_a_bad_quantity_becomes_one(db_session):
  71. row, _ = await _restore_archive(db_session, _entry(quantity="many"))
  72. assert row.quantity == 1
  73. async def test_usage_numbers_are_checked_too(db_session):
  74. tally = _CategoryTally()
  75. usage = {
  76. "usage_history": [
  77. {
  78. "spool_id": None,
  79. "weight_used": -3,
  80. "percent_used": 500,
  81. "cost": "abc",
  82. "created_at": "2026-02-01 09:00:00",
  83. }
  84. ]
  85. }
  86. await _service()._restore_spools(db_session, {"spools": []}, usage, False, tally, {})
  87. await db_session.commit()
  88. row = (await db_session.execute(select(SpoolUsageHistory))).scalar_one()
  89. assert (row.weight_used, row.cost) == (0, None)
  90. assert "usageNumbersDropped" in _codes(tally)
  91. async def test_a_quantity_of_zero_is_kept(db_session):
  92. """A ruined plate (#3051): counting it as a print again would be wrong."""
  93. row, _ = await _restore_archive(db_session, _entry(quantity=0))
  94. assert row.quantity == 0
  95. async def test_a_huge_integer_is_left_empty_not_a_crash(db_session):
  96. row, tally = await _restore_archive(db_session, _entry(print_time_seconds=10**400, cost=10**400))
  97. assert (row.print_time_seconds, row.cost) == (None, None)
  98. assert "archivesNumbersDropped" in _codes(tally)
  99. async def test_an_integer_too_large_for_the_column_is_left_empty(db_session):
  100. row, _ = await _restore_archive(db_session, _entry(print_time_seconds=2**31))
  101. assert row.print_time_seconds is None