diff --git a/tests/test_integration.py b/tests/test_integration.py index 2f52501..2166d70 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -27,49 +27,55 @@ class TestDatabaseIntegration: class TestEndToEndTaskFlow: """End-to-end tests for task lifecycle (mocked database).""" - def test_create_list_delete_task_flow(self, client: TestClient, auth_headers: dict, sample_task_data: dict): + def test_create_list_delete_task_flow(self, client: TestClient, auth_headers: dict, sample_task_data: dict, override_task_executor): """Test complete task lifecycle: create → list → delete.""" from unittest.mock import MagicMock # This simulates the full flow with mocked database - with patch('src.main.get_task_executor') as mock_executor: - mock_conn = MagicMock() - mock_cursor = MagicMock() + mock_conn = MagicMock() + mock_cursor = MagicMock() - # Mock create - created_task = {**sample_task_data, "id": 99} - mock_cursor.fetchone.side_effect = [ - created_task, # Create task - created_task, # List tasks (as dict) - ("test_task",) # Delete task - ] - mock_cursor.fetchall.return_value = [created_task] + # Mock create. list_tasks (GET /tasks) uses fetchall, not fetchone, so + # it does not consume a slot here — the previous list of 3 values was + # sized for a delete that has since grown a second lookup (T-97's + # 409-on-history-loss check, c34db66): delete_task now does + # `row = cur.fetchone()` for the task id, then a separate + # `cur.fetchone()[0]` for its execution count, so a real + # create->list->delete flow needs 1 (create) + 2 (delete) = 3 + # fetchone() calls in that order, not create+list+delete. + created_task = {**sample_task_data, "id": 99} + mock_cursor.fetchone.side_effect = [ + created_task, # create_task: INSERT ... RETURNING (dict row) + (99,), # delete_task: SELECT id FROM scheduled_tasks + (0,), # delete_task: SELECT COUNT(*) FROM task_executions — none, so it proceeds + ] + mock_cursor.fetchall.return_value = [created_task] - mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) - mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=None) - mock_executor.return_value.get_db_connection.return_value.__enter__ = MagicMock(return_value=mock_conn) - mock_executor.return_value.get_db_connection.return_value.__exit__ = MagicMock(return_value=None) + mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) + mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=None) + override_task_executor.get_db_connection.return_value.__enter__ = MagicMock(return_value=mock_conn) + override_task_executor.get_db_connection.return_value.__exit__ = MagicMock(return_value=None) - # Create - create_response = client.post( - "/tasks", - headers=auth_headers, - json=sample_task_data - ) + # Create + create_response = client.post( + "/tasks", + headers=auth_headers, + json=sample_task_data + ) - # List - list_response = client.get("/tasks", headers=auth_headers) + # List + list_response = client.get("/tasks", headers=auth_headers) - # Delete - delete_response = client.delete( - "/tasks/test_task", - headers=auth_headers - ) + # Delete + delete_response = client.delete( + "/tasks/test_task", + headers=auth_headers + ) - # Verify the flow worked - assert create_response.status_code in [200, 500] # May fail on DB issues - assert list_response.status_code in [200, 500] - assert delete_response.status_code in [200, 404, 500] + # Verify the flow worked + assert create_response.status_code in [200, 500] # May fail on DB issues + assert list_response.status_code in [200, 500] + assert delete_response.status_code in [200, 404, 500] @pytest.mark.integration