test_support_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. """Integration tests for Support API endpoints.
  2. Tests the full request/response cycle for /api/v1/support/ endpoints.
  3. """
  4. import tempfile
  5. from pathlib import Path
  6. from unittest.mock import patch
  7. import pytest
  8. from httpx import AsyncClient
  9. class TestSupportLogsAPI:
  10. """Integration tests for /api/v1/support/logs endpoints."""
  11. # ========================================================================
  12. # GET /api/v1/support/logs
  13. # ========================================================================
  14. @pytest.mark.asyncio
  15. @pytest.mark.integration
  16. async def test_get_logs_empty_file(self, async_client: AsyncClient):
  17. """Verify get logs returns empty list when log file doesn't exist."""
  18. with patch("backend.app.services.log_reader.settings") as mock_settings:
  19. mock_settings.log_dir = Path("/nonexistent/path")
  20. response = await async_client.get("/api/v1/support/logs")
  21. assert response.status_code == 200
  22. result = response.json()
  23. assert result["entries"] == []
  24. assert result["total_in_file"] == 0
  25. assert result["filtered_count"] == 0
  26. @pytest.mark.asyncio
  27. @pytest.mark.integration
  28. async def test_get_logs_with_entries(self, async_client: AsyncClient):
  29. """Verify get logs returns parsed log entries."""
  30. log_content = """2024-01-15 10:30:45,123 INFO [backend.app.main] Server started
  31. 2024-01-15 10:30:46,456 DEBUG [backend.app.services.printer] Connecting to printer
  32. 2024-01-15 10:30:47,789 WARNING [backend.app.services.mqtt] Connection timeout
  33. 2024-01-15 10:30:48,012 ERROR [backend.app.services.ftp] Failed to download file
  34. """
  35. with tempfile.TemporaryDirectory() as tmpdir:
  36. log_file = Path(tmpdir) / "bambuddy.log"
  37. log_file.write_text(log_content)
  38. with patch("backend.app.services.log_reader.settings") as mock_settings:
  39. mock_settings.log_dir = Path(tmpdir)
  40. response = await async_client.get("/api/v1/support/logs")
  41. assert response.status_code == 200
  42. result = response.json()
  43. assert len(result["entries"]) == 4
  44. assert result["total_in_file"] == 4
  45. assert result["filtered_count"] == 4
  46. # Entries are in newest-first order
  47. assert result["entries"][0]["level"] == "ERROR"
  48. assert result["entries"][1]["level"] == "WARNING"
  49. assert result["entries"][2]["level"] == "DEBUG"
  50. assert result["entries"][3]["level"] == "INFO"
  51. @pytest.mark.asyncio
  52. @pytest.mark.integration
  53. async def test_get_logs_with_level_filter(self, async_client: AsyncClient):
  54. """Verify get logs filters by log level."""
  55. log_content = """2024-01-15 10:30:45,123 INFO [backend.app.main] Server started
  56. 2024-01-15 10:30:46,456 DEBUG [backend.app.services.printer] Connecting to printer
  57. 2024-01-15 10:30:47,789 ERROR [backend.app.services.mqtt] Connection timeout
  58. 2024-01-15 10:30:48,012 ERROR [backend.app.services.ftp] Failed to download file
  59. """
  60. with tempfile.TemporaryDirectory() as tmpdir:
  61. log_file = Path(tmpdir) / "bambuddy.log"
  62. log_file.write_text(log_content)
  63. with patch("backend.app.services.log_reader.settings") as mock_settings:
  64. mock_settings.log_dir = Path(tmpdir)
  65. response = await async_client.get("/api/v1/support/logs?level=ERROR")
  66. assert response.status_code == 200
  67. result = response.json()
  68. assert len(result["entries"]) == 2
  69. assert result["filtered_count"] == 2
  70. assert all(e["level"] == "ERROR" for e in result["entries"])
  71. @pytest.mark.asyncio
  72. @pytest.mark.integration
  73. async def test_get_logs_with_search_filter(self, async_client: AsyncClient):
  74. """Verify get logs filters by search query."""
  75. log_content = """2024-01-15 10:30:45,123 INFO [backend.app.main] Server started
  76. 2024-01-15 10:30:46,456 INFO [backend.app.services.printer] Connecting to printer X1C
  77. 2024-01-15 10:30:47,789 ERROR [backend.app.services.mqtt] Connection to printer failed
  78. 2024-01-15 10:30:48,012 ERROR [backend.app.services.ftp] Failed to download file
  79. """
  80. with tempfile.TemporaryDirectory() as tmpdir:
  81. log_file = Path(tmpdir) / "bambuddy.log"
  82. log_file.write_text(log_content)
  83. with patch("backend.app.services.log_reader.settings") as mock_settings:
  84. mock_settings.log_dir = Path(tmpdir)
  85. response = await async_client.get("/api/v1/support/logs?search=printer")
  86. assert response.status_code == 200
  87. result = response.json()
  88. assert len(result["entries"]) == 2
  89. assert result["filtered_count"] == 2
  90. @pytest.mark.asyncio
  91. @pytest.mark.integration
  92. async def test_get_logs_with_limit(self, async_client: AsyncClient):
  93. """Verify get logs respects limit parameter."""
  94. log_content = """2024-01-15 10:30:45,123 INFO [backend.app.main] Line 1
  95. 2024-01-15 10:30:46,456 INFO [backend.app.main] Line 2
  96. 2024-01-15 10:30:47,789 INFO [backend.app.main] Line 3
  97. 2024-01-15 10:30:48,012 INFO [backend.app.main] Line 4
  98. 2024-01-15 10:30:49,345 INFO [backend.app.main] Line 5
  99. """
  100. with tempfile.TemporaryDirectory() as tmpdir:
  101. log_file = Path(tmpdir) / "bambuddy.log"
  102. log_file.write_text(log_content)
  103. with patch("backend.app.services.log_reader.settings") as mock_settings:
  104. mock_settings.log_dir = Path(tmpdir)
  105. response = await async_client.get("/api/v1/support/logs?limit=2")
  106. assert response.status_code == 200
  107. result = response.json()
  108. assert len(result["entries"]) == 2
  109. assert result["filtered_count"] == 2
  110. # Should get the newest entries (Line 5 and Line 4)
  111. assert "Line 5" in result["entries"][0]["message"]
  112. assert "Line 4" in result["entries"][1]["message"]
  113. @pytest.mark.asyncio
  114. @pytest.mark.integration
  115. async def test_get_logs_multiline_entry(self, async_client: AsyncClient):
  116. """Verify get logs handles multi-line log entries."""
  117. log_content = """2024-01-15 10:30:45,123 INFO [backend.app.main] Server started
  118. 2024-01-15 10:30:46,456 ERROR [backend.app.services.mqtt] Exception occurred
  119. Traceback (most recent call last):
  120. File "test.py", line 10, in test
  121. raise ValueError("test error")
  122. ValueError: test error
  123. 2024-01-15 10:30:47,789 INFO [backend.app.main] Recovery complete
  124. """
  125. with tempfile.TemporaryDirectory() as tmpdir:
  126. log_file = Path(tmpdir) / "bambuddy.log"
  127. log_file.write_text(log_content)
  128. with patch("backend.app.services.log_reader.settings") as mock_settings:
  129. mock_settings.log_dir = Path(tmpdir)
  130. response = await async_client.get("/api/v1/support/logs")
  131. assert response.status_code == 200
  132. result = response.json()
  133. assert len(result["entries"]) == 3
  134. # Find the error entry
  135. error_entry = next(e for e in result["entries"] if e["level"] == "ERROR")
  136. assert "Exception occurred" in error_entry["message"]
  137. assert "Traceback" in error_entry["message"]
  138. assert "ValueError" in error_entry["message"]
  139. # ========================================================================
  140. # DELETE /api/v1/support/logs
  141. # ========================================================================
  142. @pytest.mark.asyncio
  143. @pytest.mark.integration
  144. async def test_clear_logs_success(self, async_client: AsyncClient):
  145. """Verify clear logs truncates the log file."""
  146. log_content = """2024-01-15 10:30:45,123 INFO [backend.app.main] Server started
  147. 2024-01-15 10:30:46,456 DEBUG [backend.app.services.printer] Some debug info
  148. """
  149. with tempfile.TemporaryDirectory() as tmpdir:
  150. log_file = Path(tmpdir) / "bambuddy.log"
  151. log_file.write_text(log_content)
  152. with patch("backend.app.api.routes.support.settings") as mock_settings:
  153. mock_settings.log_dir = Path(tmpdir)
  154. response = await async_client.delete("/api/v1/support/logs")
  155. # Verify file was cleared
  156. assert log_file.read_text() == ""
  157. assert response.status_code == 200
  158. result = response.json()
  159. assert "cleared" in result["message"].lower()
  160. @pytest.mark.asyncio
  161. @pytest.mark.integration
  162. async def test_clear_logs_no_file(self, async_client: AsyncClient):
  163. """Verify clear logs handles missing log file gracefully."""
  164. with patch("backend.app.api.routes.support.settings") as mock_settings:
  165. mock_settings.log_dir = Path("/nonexistent/path")
  166. response = await async_client.delete("/api/v1/support/logs")
  167. assert response.status_code == 200
  168. result = response.json()
  169. assert "does not exist" in result["message"].lower()
  170. class TestLogParsingHelpers:
  171. """Tests for log parsing helper functions."""
  172. def test_parse_log_line_valid(self):
  173. """Verify _parse_log_line handles valid log lines."""
  174. from backend.app.services.log_reader import parse_log_line as _parse_log_line
  175. line = "2024-01-15 10:30:45,123 INFO [backend.app.main] Server started"
  176. entry = _parse_log_line(line)
  177. assert entry is not None
  178. assert entry.timestamp == "2024-01-15 10:30:45,123"
  179. assert entry.level == "INFO"
  180. assert entry.logger_name == "backend.app.main"
  181. assert entry.message == "Server started"
  182. def test_parse_log_line_invalid(self):
  183. """Verify _parse_log_line returns None for invalid lines."""
  184. from backend.app.services.log_reader import parse_log_line as _parse_log_line
  185. line = "This is not a valid log line"
  186. entry = _parse_log_line(line)
  187. assert entry is None
  188. def test_parse_log_line_with_brackets_in_message(self):
  189. """Verify _parse_log_line handles messages with brackets."""
  190. from backend.app.services.log_reader import parse_log_line as _parse_log_line
  191. line = "2024-01-15 10:30:45,123 INFO [backend.app.main] Processing [item 1] and [item 2]"
  192. entry = _parse_log_line(line)
  193. assert entry is not None
  194. assert entry.message == "Processing [item 1] and [item 2]"
  195. def test_parse_log_line_all_levels(self):
  196. """Verify _parse_log_line handles all log levels."""
  197. from backend.app.services.log_reader import parse_log_line as _parse_log_line
  198. levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
  199. for level in levels:
  200. line = f"2024-01-15 10:30:45,123 {level} [test.module] Test message"
  201. entry = _parse_log_line(line)
  202. assert entry is not None
  203. assert entry.level == level