test_ams_history_api.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """Integration tests for AMS History API endpoints."""
  2. from datetime import datetime, timedelta
  3. import pytest
  4. from httpx import AsyncClient
  5. class TestAMSHistoryAPI:
  6. """Integration tests for /api/v1/ams-history endpoints."""
  7. @pytest.fixture
  8. async def ams_history_factory(self, db_session, printer_factory):
  9. """Factory to create test AMS history records."""
  10. async def _create_history(printer_id=None, ams_id=0, **kwargs):
  11. from backend.app.models.ams_history import AMSSensorHistory
  12. if printer_id is None:
  13. printer = await printer_factory()
  14. printer_id = printer.id
  15. defaults = {
  16. "printer_id": printer_id,
  17. "ams_id": ams_id,
  18. "humidity": 45.0,
  19. "humidity_raw": 4500,
  20. "temperature": 25.0,
  21. "recorded_at": datetime.now(),
  22. }
  23. defaults.update(kwargs)
  24. history = AMSSensorHistory(**defaults)
  25. db_session.add(history)
  26. await db_session.commit()
  27. await db_session.refresh(history)
  28. return history
  29. return _create_history
  30. @pytest.mark.asyncio
  31. @pytest.mark.integration
  32. async def test_get_ams_history_empty(self, async_client: AsyncClient, printer_factory, db_session):
  33. """Verify empty history returns empty data array."""
  34. printer = await printer_factory()
  35. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
  36. assert response.status_code == 200
  37. data = response.json()
  38. assert data["printer_id"] == printer.id
  39. assert data["ams_id"] == 0
  40. assert data["data"] == []
  41. @pytest.mark.asyncio
  42. @pytest.mark.integration
  43. async def test_get_ams_history_with_data(self, async_client: AsyncClient, ams_history_factory, db_session):
  44. """Verify history returns recorded data."""
  45. # Create history records
  46. history = await ams_history_factory()
  47. printer_id = history.printer_id
  48. response = await async_client.get(f"/api/v1/ams-history/{printer_id}/0")
  49. assert response.status_code == 200
  50. data = response.json()
  51. assert len(data["data"]) >= 1
  52. @pytest.mark.asyncio
  53. @pytest.mark.integration
  54. async def test_get_ams_history_with_stats(
  55. self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
  56. ):
  57. """Verify history includes statistics."""
  58. printer = await printer_factory()
  59. # Create multiple records with different values
  60. await ams_history_factory(printer_id=printer.id, humidity=40.0, temperature=24.0)
  61. await ams_history_factory(printer_id=printer.id, humidity=50.0, temperature=26.0)
  62. await ams_history_factory(printer_id=printer.id, humidity=45.0, temperature=25.0)
  63. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
  64. assert response.status_code == 200
  65. data = response.json()
  66. # Check statistics
  67. assert data["min_humidity"] == 40.0
  68. assert data["max_humidity"] == 50.0
  69. assert data["min_temperature"] == 24.0
  70. assert data["max_temperature"] == 26.0
  71. @pytest.mark.asyncio
  72. @pytest.mark.integration
  73. async def test_an_average_of_zero_is_reported_as_zero(
  74. self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
  75. ):
  76. """A window whose readings are all 0 has an average of 0, not "no data".
  77. The response used to test the average for truthiness, so min and max
  78. reported 0.0 while the average beside them came back null and the card
  79. showed an em dash (#3140). Zero is rare but real -- a warm unit part way
  80. through a drying cycle reaches it.
  81. """
  82. printer = await printer_factory()
  83. await ams_history_factory(printer_id=printer.id, humidity=0.0, temperature=0.0)
  84. await ams_history_factory(printer_id=printer.id, humidity=0.0, temperature=0.0)
  85. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
  86. assert response.status_code == 200
  87. data = response.json()
  88. assert data["min_humidity"] == 0.0
  89. assert data["avg_humidity"] == 0.0
  90. assert data["avg_temperature"] == 0.0
  91. @pytest.mark.asyncio
  92. @pytest.mark.integration
  93. async def test_an_empty_window_still_has_no_average(self, async_client: AsyncClient, printer_factory):
  94. """The one case that genuinely has no answer must stay null."""
  95. printer = await printer_factory()
  96. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
  97. assert response.status_code == 200
  98. data = response.json()
  99. assert data["data"] == []
  100. assert data["avg_humidity"] is None
  101. assert data["avg_temperature"] is None
  102. @pytest.mark.asyncio
  103. @pytest.mark.integration
  104. async def test_get_ams_history_with_hours_filter(
  105. self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
  106. ):
  107. """Verify hours parameter filters data."""
  108. printer = await printer_factory()
  109. # Create a recent record
  110. await ams_history_factory(printer_id=printer.id, recorded_at=datetime.now())
  111. # Create an old record (outside default 24h)
  112. await ams_history_factory(printer_id=printer.id, recorded_at=datetime.now() - timedelta(hours=48))
  113. # Request only last 24 hours (default)
  114. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
  115. assert response.status_code == 200
  116. data = response.json()
  117. # Should only get the recent record
  118. assert len(data["data"]) == 1
  119. @pytest.mark.asyncio
  120. @pytest.mark.integration
  121. async def test_get_ams_history_custom_hours(self, async_client: AsyncClient, printer_factory, db_session):
  122. """Verify custom hours parameter works."""
  123. printer = await printer_factory()
  124. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0", params={"hours": 48})
  125. assert response.status_code == 200
  126. data = response.json()
  127. assert data["printer_id"] == printer.id
  128. @pytest.mark.asyncio
  129. @pytest.mark.integration
  130. async def test_get_ams_history_different_ams_units(
  131. self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
  132. ):
  133. """Verify filtering by AMS unit ID."""
  134. printer = await printer_factory()
  135. await ams_history_factory(printer_id=printer.id, ams_id=0, humidity=40.0)
  136. await ams_history_factory(printer_id=printer.id, ams_id=1, humidity=50.0)
  137. # Get AMS unit 0
  138. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/0")
  139. assert response.status_code == 200
  140. data0 = response.json()
  141. assert len(data0["data"]) == 1
  142. assert data0["data"][0]["humidity"] == 40.0
  143. # Get AMS unit 1
  144. response = await async_client.get(f"/api/v1/ams-history/{printer.id}/1")
  145. assert response.status_code == 200
  146. data1 = response.json()
  147. assert len(data1["data"]) == 1
  148. assert data1["data"][0]["humidity"] == 50.0
  149. @pytest.mark.asyncio
  150. @pytest.mark.integration
  151. async def test_delete_old_history(
  152. self, async_client: AsyncClient, ams_history_factory, printer_factory, db_session
  153. ):
  154. """Verify old history can be deleted."""
  155. printer = await printer_factory()
  156. # Create an old record
  157. await ams_history_factory(printer_id=printer.id, recorded_at=datetime.now() - timedelta(days=60))
  158. # Delete records older than 30 days
  159. response = await async_client.delete(f"/api/v1/ams-history/{printer.id}", params={"days": 30})
  160. assert response.status_code == 200
  161. data = response.json()
  162. assert data["deleted"] >= 1
  163. @pytest.mark.asyncio
  164. @pytest.mark.integration
  165. async def test_delete_old_history_no_records(self, async_client: AsyncClient, printer_factory, db_session):
  166. """Verify delete with no old records returns 0."""
  167. printer = await printer_factory()
  168. response = await async_client.delete(f"/api/v1/ams-history/{printer.id}", params={"days": 30})
  169. assert response.status_code == 200
  170. data = response.json()
  171. assert data["deleted"] == 0