conftest.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. """
  2. Unit test conftest.py - Fixtures specific to unit tests.
  3. Provides fixtures for mocking FastAPI dependencies and isolating tests
  4. from real state and hardware connections.
  5. """
  6. import pytest
  7. from unittest.mock import MagicMock, AsyncMock, patch
  8. @pytest.fixture
  9. def mock_state_unit():
  10. """Mock state for unit tests with common defaults.
  11. This is a more comprehensive mock than the root conftest version,
  12. specifically designed for API endpoint testing where we need to
  13. control the application state precisely.
  14. """
  15. mock = MagicMock()
  16. # Connection mock
  17. mock.conn = MagicMock()
  18. mock.conn.is_connected.return_value = False
  19. mock.port = None
  20. mock.is_connected = False
  21. mock.preferred_port = None
  22. # Pattern execution state
  23. mock.current_playing_file = None
  24. mock.is_running = False
  25. mock.pause_requested = False
  26. mock.stop_requested = False
  27. mock.skip_requested = False
  28. mock.execution_progress = None
  29. mock.is_homing = False
  30. mock.is_clearing = False
  31. # Position state
  32. mock.current_theta = 0.0
  33. mock.current_rho = 0.0
  34. mock.machine_x = 0.0
  35. mock.machine_y = 0.0
  36. # Speed and settings
  37. mock.speed = 100
  38. mock.clear_pattern_speed = None
  39. mock.table_type = "dune_weaver"
  40. mock.table_type_override = None
  41. mock.homing = 0
  42. # Playlist state
  43. mock.current_playlist = None
  44. mock.current_playlist_name = None
  45. mock.current_playlist_index = None
  46. mock.playlist_mode = None
  47. mock.pause_time_remaining = 0
  48. mock.original_pause_time = None
  49. # LED state
  50. mock.led_controller = None
  51. mock.led_provider = "none"
  52. mock.wled_ip = None
  53. mock.hyperion_ip = None
  54. mock.hyperion_port = 19444
  55. mock.dw_led_num_leds = 60
  56. mock.dw_led_gpio_pin = 18
  57. mock.dw_led_pixel_order = "GRB"
  58. mock.dw_led_brightness = 50
  59. mock.dw_led_speed = 128
  60. mock.dw_led_intensity = 128
  61. mock.dw_led_idle_effect = "solid"
  62. mock.dw_led_playing_effect = "rainbow"
  63. mock.dw_led_idle_timeout_enabled = False
  64. mock.dw_led_idle_timeout_minutes = 30
  65. mock.dw_led_last_activity_time = 0
  66. # Scheduled pause
  67. mock.scheduled_pause_enabled = False
  68. mock.scheduled_pause_time_slots = []
  69. mock.scheduled_pause_control_wled = False
  70. mock.scheduled_pause_finish_pattern = False
  71. mock.scheduled_pause_timezone = None
  72. # Gear ratio
  73. mock.gear_ratio = 10.0
  74. # Auto-home settings
  75. mock.auto_home_enabled = False
  76. mock.auto_home_after_patterns = 10
  77. mock.patterns_since_last_home = 0
  78. # Custom clear patterns
  79. mock.custom_clear_from_out = None
  80. mock.custom_clear_from_in = None
  81. # Homing offset
  82. mock.angular_homing_offset_degrees = 0.0
  83. # App settings
  84. mock.app_name = "Dune Weaver"
  85. mock.custom_logo_path = None
  86. # MQTT settings
  87. mock.mqtt_enabled = False
  88. mock.mqtt_broker = None
  89. mock.mqtt_port = 1883
  90. mock.mqtt_username = None
  91. mock.mqtt_password = None
  92. mock.mqtt_topic_prefix = "dune_weaver"
  93. # Table info
  94. mock.table_id = None
  95. mock.table_name = None
  96. mock.known_tables = []
  97. # Methods
  98. mock.save = MagicMock()
  99. mock.get_stop_event = MagicMock(return_value=None)
  100. mock.get_skip_event = MagicMock(return_value=None)
  101. mock.wait_for_interrupt = AsyncMock(return_value='timeout')
  102. mock.pause_condition = MagicMock()
  103. mock.pause_condition.__enter__ = MagicMock()
  104. mock.pause_condition.__exit__ = MagicMock()
  105. mock.pause_condition.notify_all = MagicMock()
  106. return mock
  107. @pytest.fixture
  108. def mock_connection_unit():
  109. """Mock connection for unit tests.
  110. Provides a connection mock that simulates a connected device
  111. without requiring actual hardware.
  112. """
  113. mock = MagicMock()
  114. mock.is_connected.return_value = True
  115. mock.send = MagicMock()
  116. mock.readline = MagicMock(return_value="ok")
  117. mock.in_waiting = MagicMock(return_value=0)
  118. mock.flush = MagicMock()
  119. mock.close = MagicMock()
  120. mock.reset_input_buffer = MagicMock()
  121. return mock
  122. @pytest.fixture
  123. def app_with_mocked_state(mock_state_unit):
  124. """Fixture that patches state module before importing app.
  125. This ensures the app uses mocked state for all operations.
  126. Must be used before creating async_client.
  127. """
  128. with patch("modules.core.state.state", mock_state_unit):
  129. with patch("modules.core.pattern_manager.state", mock_state_unit):
  130. with patch("modules.core.playlist_manager.state", mock_state_unit):
  131. with patch("modules.connection.connection_manager.state", mock_state_unit):
  132. from main import app
  133. yield app, mock_state_unit
  134. @pytest.fixture
  135. async def async_client_with_mocked_state(app_with_mocked_state):
  136. """AsyncClient with mocked state for isolated API testing.
  137. This fixture combines the app patching with the async client creation.
  138. """
  139. from httpx import ASGITransport, AsyncClient
  140. app, mock_state = app_with_mocked_state
  141. async with AsyncClient(
  142. transport=ASGITransport(app=app),
  143. base_url="http://test"
  144. ) as client:
  145. yield client, mock_state
  146. @pytest.fixture
  147. def cleanup_app_overrides():
  148. """Fixture to ensure app.dependency_overrides is cleaned up after tests."""
  149. from main import app
  150. yield
  151. # Cleanup after test
  152. app.dependency_overrides.clear()