FastAPI resolves Depends(get_task_executor) against the function object it
captured when each route was decorated, at import time. patch('src.main.
get_task_executor') therefore never reaches an already-registered route —
confirmed empirically with a minimal FastAPI app before touching this repo's
tests. Every one of these tests fell through to the real dependency, which
called the real psycopg2.connect() and failed on postgres-shared's DNS (a
Docker-network name unreachable from a host process either way, so this
reproduced identically with and without host networking).
app.dependency_overrides is FastAPI's own supported mechanism for this, and
was already used correctly elsewhere in this suite (test_task_delete.py).
Added a shared override_task_executor fixture in conftest.py and switched
every get_task_executor patch() call site in test_api_comprehensive.py to it.
Source (src/main.py) is unchanged — Depends() is the idiomatic, correct
pattern; the tests were using an ineffective substitute for it.
300 lines
13 KiB
Python
300 lines
13 KiB
Python
"""
|
|
Comprehensive API tests to improve coverage of main.py.
|
|
"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import MagicMock
|
|
import json
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestTaskCRUDOperations:
|
|
"""Comprehensive CRUD tests for task endpoints."""
|
|
|
|
def test_list_tasks_empty(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test listing tasks when none exist."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchall.return_value = []
|
|
|
|
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)
|
|
|
|
response = client.get("/tasks", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert "tasks" in data
|
|
assert data["count"] == 0
|
|
|
|
def test_list_tasks_with_filters(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test listing tasks with enabled and service filters."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchall.return_value = []
|
|
|
|
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)
|
|
|
|
response = client.get(
|
|
"/tasks?enabled=true&service=scheduler",
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Should execute filtered query
|
|
assert mock_cursor.execute.called
|
|
call_args = str(mock_cursor.execute.call_args)
|
|
assert "enabled" in call_args.lower() or response.status_code in [200, 500]
|
|
|
|
def test_get_task_details_not_found(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test getting details for non-existent task."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.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)
|
|
|
|
response = client.get("/tasks/nonexistent", headers=auth_headers)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_update_task(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test updating a task."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.return_value = {
|
|
"task_name": "test",
|
|
"priority": 60
|
|
}
|
|
|
|
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)
|
|
|
|
response = client.put(
|
|
"/tasks/test",
|
|
headers=auth_headers,
|
|
json={"priority": 60}
|
|
)
|
|
|
|
# Should have attempted update
|
|
assert mock_cursor.execute.called
|
|
|
|
def test_update_task_not_found(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test updating non-existent task."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.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)
|
|
|
|
response = client.put(
|
|
"/tasks/nonexistent",
|
|
headers=auth_headers,
|
|
json={"priority": 60}
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
def test_update_task_no_fields(self, client: TestClient, auth_headers: dict):
|
|
"""Test updating task with no valid fields."""
|
|
response = client.put(
|
|
"/tasks/test",
|
|
headers=auth_headers,
|
|
json={"invalid_field": "value"}
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
def test_delete_task(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test deleting a task."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.return_value = ("test_task",)
|
|
|
|
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)
|
|
|
|
response = client.delete("/tasks/test_task", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert "deleted successfully" in data["message"].lower()
|
|
|
|
def test_delete_task_not_found(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test deleting non-existent task."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.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)
|
|
|
|
response = client.delete("/tasks/nonexistent", headers=auth_headers)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestTriggerEndpoint:
|
|
"""Tests for task trigger endpoint."""
|
|
|
|
def test_trigger_disabled_task(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test triggering a disabled task."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.return_value = {
|
|
"task_name": "test",
|
|
"enabled": False
|
|
}
|
|
|
|
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)
|
|
|
|
response = client.post("/tasks/test/trigger", headers=auth_headers)
|
|
|
|
assert response.status_code == 400
|
|
|
|
def test_trigger_nonexistent_task(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test triggering a task that doesn't exist."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.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)
|
|
|
|
response = client.post("/tasks/nonexistent/trigger", headers=auth_headers)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestLegacyEndpoints:
|
|
"""Tests for deprecated/legacy endpoints."""
|
|
|
|
def test_trigger_backup_endpoint(self, client: TestClient, auth_headers: dict):
|
|
"""Test legacy backup trigger endpoint."""
|
|
response = client.post("/tasks/backup", headers=auth_headers)
|
|
|
|
# Should return not implemented status
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert data["status"] == "not_implemented"
|
|
|
|
def test_trigger_docs_update_endpoint(self, client: TestClient, auth_headers: dict):
|
|
"""Test legacy docs update endpoint."""
|
|
response = client.post("/tasks/docs/update", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert data["status"] == "not_implemented"
|
|
|
|
def test_trigger_docs_update_with_project(self, client: TestClient, auth_headers: dict):
|
|
"""Test legacy docs update with project filter."""
|
|
response = client.post(
|
|
"/tasks/docs/update?project=test",
|
|
headers=auth_headers
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert "test" in data["message"].lower()
|
|
|
|
def test_check_doc_versions_endpoint(self, client: TestClient, auth_headers: dict):
|
|
"""Test legacy version check endpoint."""
|
|
response = client.post("/tasks/docs/check-versions", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert data["status"] == "not_implemented"
|
|
|
|
def test_trigger_cleanup_endpoint(self, client: TestClient, auth_headers: dict):
|
|
"""Test legacy cleanup endpoint."""
|
|
response = client.post("/tasks/cleanup", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert data["status"] == "not_implemented"
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestExecutionFiltering:
|
|
"""Tests for execution history filtering."""
|
|
|
|
def test_filter_by_service(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test filtering executions by service."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchall.return_value = []
|
|
|
|
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)
|
|
|
|
response = client.get(
|
|
"/executions?service=scheduler",
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert mock_cursor.execute.called
|
|
|
|
def test_filter_by_status(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test filtering executions by status."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchall.return_value = []
|
|
|
|
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)
|
|
|
|
response = client.get(
|
|
"/executions?status=success",
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert mock_cursor.execute.called
|
|
|
|
def test_custom_limit(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test custom limit for executions."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchall.return_value = []
|
|
|
|
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)
|
|
|
|
response = client.get("/executions?limit=50", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert data["limit"] == 50
|