|
|
@@ -212,17 +212,16 @@ class TestBugReportService:
|
|
|
assert "GitHub API error" in result["message"]
|
|
|
|
|
|
|
|
|
-class TestCollectDebugLogs:
|
|
|
- """Tests for _collect_debug_logs()."""
|
|
|
+class TestStartLogging:
|
|
|
+ """Tests for the start-logging endpoint handler."""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.unit
|
|
|
async def test_enables_debug_when_not_already_enabled(self):
|
|
|
- """Debug logging is enabled, then restored after collection."""
|
|
|
- from backend.app.api.routes.bug_report import _collect_debug_logs
|
|
|
+ """Debug logging is enabled and printers are pushed."""
|
|
|
+ from backend.app.api.routes.bug_report import start_logging
|
|
|
|
|
|
apply_calls = []
|
|
|
-
|
|
|
mock_db = AsyncMock()
|
|
|
|
|
|
with (
|
|
|
@@ -234,25 +233,26 @@ class TestCollectDebugLogs:
|
|
|
side_effect=lambda v: apply_calls.append(v),
|
|
|
),
|
|
|
patch("backend.app.api.routes.bug_report.printer_manager") as mock_pm,
|
|
|
- patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="DEBUG log line"),
|
|
|
- patch("backend.app.api.routes.bug_report.asyncio.sleep", new_callable=AsyncMock),
|
|
|
- patch("backend.app.api.routes.bug_report.LOG_COLLECTION_SECONDS", 0),
|
|
|
):
|
|
|
- mock_pm._clients = {}
|
|
|
+ mock_pm._clients = {"printer1": MagicMock()}
|
|
|
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
|
|
|
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
|
|
- result = await _collect_debug_logs()
|
|
|
+ result = await start_logging()
|
|
|
|
|
|
- assert result == "DEBUG log line"
|
|
|
- assert apply_calls == [True, False] # enabled then restored
|
|
|
- assert mock_set.call_count == 2
|
|
|
+ assert result.started is True
|
|
|
+ assert result.was_debug is False
|
|
|
+ assert apply_calls == [True]
|
|
|
+ mock_set.assert_called_once()
|
|
|
+ mock_pm.request_status_update.assert_called_once_with("printer1")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
@pytest.mark.unit
|
|
|
async def test_skips_enable_when_already_debug(self):
|
|
|
"""Debug logging not toggled when already enabled."""
|
|
|
- from backend.app.api.routes.bug_report import _collect_debug_logs
|
|
|
+ mock_db = AsyncMock()
|
|
|
+
|
|
|
+ from backend.app.api.routes.bug_report import start_logging
|
|
|
|
|
|
with (
|
|
|
patch("backend.app.api.routes.bug_report.async_session") as mock_session,
|
|
|
@@ -260,18 +260,15 @@ class TestCollectDebugLogs:
|
|
|
patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock) as mock_set,
|
|
|
patch("backend.app.api.routes.bug_report._apply_log_level") as mock_apply,
|
|
|
patch("backend.app.api.routes.bug_report.printer_manager") as mock_pm,
|
|
|
- patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="logs"),
|
|
|
- patch("backend.app.api.routes.bug_report.asyncio.sleep", new_callable=AsyncMock),
|
|
|
- patch("backend.app.api.routes.bug_report.LOG_COLLECTION_SECONDS", 0),
|
|
|
):
|
|
|
mock_pm._clients = {}
|
|
|
- mock_db = AsyncMock()
|
|
|
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
|
|
|
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
|
|
- result = await _collect_debug_logs()
|
|
|
+ result = await start_logging()
|
|
|
|
|
|
- assert result == "logs"
|
|
|
+ assert result.started is True
|
|
|
+ assert result.was_debug is True
|
|
|
mock_apply.assert_not_called()
|
|
|
mock_set.assert_not_called()
|
|
|
|
|
|
@@ -279,7 +276,9 @@ class TestCollectDebugLogs:
|
|
|
@pytest.mark.unit
|
|
|
async def test_pushes_all_connected_printers(self):
|
|
|
"""Sends status update request to all connected printers."""
|
|
|
- from backend.app.api.routes.bug_report import _collect_debug_logs
|
|
|
+ mock_db = AsyncMock()
|
|
|
+
|
|
|
+ from backend.app.api.routes.bug_report import start_logging
|
|
|
|
|
|
with (
|
|
|
patch("backend.app.api.routes.bug_report.async_session") as mock_session,
|
|
|
@@ -287,20 +286,125 @@ class TestCollectDebugLogs:
|
|
|
patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock),
|
|
|
patch("backend.app.api.routes.bug_report._apply_log_level"),
|
|
|
patch("backend.app.api.routes.bug_report.printer_manager") as mock_pm,
|
|
|
- patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value=""),
|
|
|
- patch("backend.app.api.routes.bug_report.asyncio.sleep", new_callable=AsyncMock),
|
|
|
- patch("backend.app.api.routes.bug_report.LOG_COLLECTION_SECONDS", 0),
|
|
|
):
|
|
|
mock_pm._clients = {"printer1": MagicMock(), "printer2": MagicMock()}
|
|
|
- mock_db = AsyncMock()
|
|
|
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
|
|
|
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
|
|
- await _collect_debug_logs()
|
|
|
+ await start_logging()
|
|
|
|
|
|
assert mock_pm.request_status_update.call_count == 2
|
|
|
|
|
|
|
|
|
+class TestStopLogging:
|
|
|
+ """Tests for the stop-logging endpoint handler."""
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.unit
|
|
|
+ async def test_collects_logs_and_restores_level(self):
|
|
|
+ """Collects logs and restores log level when was_debug=False."""
|
|
|
+ from backend.app.api.routes.bug_report import stop_logging
|
|
|
+
|
|
|
+ apply_calls = []
|
|
|
+ mock_db = AsyncMock()
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.bug_report.async_session") as mock_session,
|
|
|
+ patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock) as mock_set,
|
|
|
+ patch(
|
|
|
+ "backend.app.api.routes.bug_report._apply_log_level",
|
|
|
+ side_effect=lambda v: apply_calls.append(v),
|
|
|
+ ),
|
|
|
+ patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="DEBUG log line"),
|
|
|
+ ):
|
|
|
+ mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
|
|
|
+ mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
|
+
|
|
|
+ result = await stop_logging(was_debug=False)
|
|
|
+
|
|
|
+ assert result.logs == "DEBUG log line"
|
|
|
+ assert apply_calls == [False]
|
|
|
+ mock_set.assert_called_once()
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.unit
|
|
|
+ async def test_skips_restore_when_was_debug(self):
|
|
|
+ """Does not restore log level when was_debug=True."""
|
|
|
+ from backend.app.api.routes.bug_report import stop_logging
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.bug_report.async_session") as mock_session,
|
|
|
+ patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock) as mock_set,
|
|
|
+ patch("backend.app.api.routes.bug_report._apply_log_level") as mock_apply,
|
|
|
+ patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="logs"),
|
|
|
+ ):
|
|
|
+ mock_db = AsyncMock()
|
|
|
+ mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
|
|
|
+ mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
|
|
|
+
|
|
|
+ result = await stop_logging(was_debug=True)
|
|
|
+
|
|
|
+ assert result.logs == "logs"
|
|
|
+ mock_apply.assert_not_called()
|
|
|
+ mock_set.assert_not_called()
|
|
|
+
|
|
|
+
|
|
|
+class TestSubmitBugReportRoute:
|
|
|
+ """Tests for the submit_bug_report route handler."""
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.unit
|
|
|
+ async def test_uses_provided_debug_logs(self):
|
|
|
+ """When debug_logs is provided, it is used as recent_logs."""
|
|
|
+ from backend.app.api.routes.bug_report import BugReportRequest, submit_bug_report
|
|
|
+
|
|
|
+ report = BugReportRequest(
|
|
|
+ description="Test bug",
|
|
|
+ debug_logs="pre-collected debug logs",
|
|
|
+ )
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.bug_report._collect_support_info", return_value={"version": "1.0"}),
|
|
|
+ patch("backend.app.api.routes.bug_report.submit_report", new_callable=AsyncMock) as mock_submit,
|
|
|
+ ):
|
|
|
+ mock_submit.return_value = {
|
|
|
+ "success": True,
|
|
|
+ "message": "Created",
|
|
|
+ "issue_url": "https://github.com/maziggy/bambuddy/issues/1",
|
|
|
+ "issue_number": 1,
|
|
|
+ }
|
|
|
+
|
|
|
+ result = await submit_bug_report(report)
|
|
|
+
|
|
|
+ assert result.success is True
|
|
|
+ call_kwargs = mock_submit.call_args[1]
|
|
|
+ assert call_kwargs["support_info"]["recent_logs"] == "pre-collected debug logs"
|
|
|
+
|
|
|
+ @pytest.mark.asyncio
|
|
|
+ @pytest.mark.unit
|
|
|
+ async def test_no_logs_when_debug_logs_not_provided(self):
|
|
|
+ """When debug_logs is None, recent_logs is not added."""
|
|
|
+ from backend.app.api.routes.bug_report import BugReportRequest, submit_bug_report
|
|
|
+
|
|
|
+ report = BugReportRequest(description="Test bug")
|
|
|
+
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.bug_report._collect_support_info", return_value={"version": "1.0"}),
|
|
|
+ patch("backend.app.api.routes.bug_report.submit_report", new_callable=AsyncMock) as mock_submit,
|
|
|
+ ):
|
|
|
+ mock_submit.return_value = {
|
|
|
+ "success": True,
|
|
|
+ "message": "Created",
|
|
|
+ "issue_url": None,
|
|
|
+ "issue_number": None,
|
|
|
+ }
|
|
|
+
|
|
|
+ await submit_bug_report(report)
|
|
|
+
|
|
|
+ call_kwargs = mock_submit.call_args[1]
|
|
|
+ assert "recent_logs" not in call_kwargs["support_info"]
|
|
|
+
|
|
|
+
|
|
|
class TestRateLimit:
|
|
|
"""Tests for rate limiting in bug report service."""
|
|
|
|