1. get_task_executor mocking: same dependency_overrides fix as the previous
commit, applied to this file's remaining patch('src.main.get_task_executor')
call sites (test_valid_api_key_allows_access, test_create_task_with_valid_data,
test_trigger_task_endpoint, test_stats_endpoint_returns_metrics,
test_executions_endpoint_returns_history, test_executions_filter_by_task_name).
2. test_create_task_missing_fields_returns_400 asserted a status this endpoint
cannot return. `task: TaskCreate` in src/main.py is a plain Pydantic request
body with no custom validation for missing fields — FastAPI's own
dependency-resolution layer rejects the request before create_task's body
runs, and that layer always answers 422, not 400. There is no code path in
this repo, at any point in its git history, that produces 400 for this
request. Renamed to test_create_task_missing_fields_returns_422 and the
assertion updated to match; source unchanged.
Kept together in one commit because both live in the same small file and were
found in the same pass, rather than risk a manual hunk split on tightly
interleaved diff context.
241 lines
9.7 KiB
Python
241 lines
9.7 KiB
Python
"""
|
|
Tests for The Scheduler API endpoints.
|
|
"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch, MagicMock
|
|
import json
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestHealthEndpoint:
|
|
"""Tests for /health endpoint."""
|
|
|
|
def test_health_endpoint_returns_healthy(self, client: TestClient):
|
|
"""Test that health endpoint returns healthy status."""
|
|
with patch('src.main.get_scheduler') as mock_get_scheduler:
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler.running = True
|
|
mock_scheduler.get_jobs.return_value = [MagicMock()]
|
|
mock_get_scheduler.return_value = mock_scheduler
|
|
|
|
response = client.get("/health")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "healthy"
|
|
assert data["scheduler_running"] is True
|
|
assert data["jobs_count"] == 1
|
|
|
|
def test_health_endpoint_no_auth_required(self, client: TestClient):
|
|
"""Test that health endpoint doesn't require authentication."""
|
|
with patch('src.main.get_scheduler') as mock_get_scheduler:
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler.running = True
|
|
mock_get_scheduler.return_value = mock_scheduler
|
|
|
|
response = client.get("/health")
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestAuthenticationEndpoints:
|
|
"""Tests for API authentication."""
|
|
|
|
def test_missing_api_key_returns_401(self, client: TestClient):
|
|
"""Test that missing API key returns 401."""
|
|
response = client.get("/tasks")
|
|
assert response.status_code == 401
|
|
|
|
def test_invalid_api_key_returns_403(self, client: TestClient):
|
|
"""Test that invalid API key returns 403."""
|
|
response = client.get(
|
|
"/tasks",
|
|
headers={"Authorization": "Bearer wrong-key"}
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_valid_api_key_allows_access(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test that valid API key allows access."""
|
|
override_task_executor.get_db_connection.return_value.__enter__.return_value.cursor.return_value.__enter__.return_value.fetchall.return_value = []
|
|
|
|
response = client.get("/tasks", headers=auth_headers)
|
|
# May fail with 500 due to DB, but should not be 401/403
|
|
assert response.status_code not in [401, 403]
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestTaskEndpoints:
|
|
"""Tests for task management endpoints."""
|
|
|
|
def test_create_task_missing_fields_returns_422(self, client: TestClient, auth_headers: dict):
|
|
"""Test that creating task without required fields returns 422.
|
|
|
|
`task: TaskCreate` (src/main.py) is a plain Pydantic request body with
|
|
no custom validation — FastAPI's own dependency-resolution layer
|
|
rejects a request missing required fields before create_task's body
|
|
ever runs, and that layer always answers 422, never 400. There is no
|
|
code path in this repo that could produce 400 here; renamed rather
|
|
than asserting a status this endpoint cannot return.
|
|
"""
|
|
incomplete_task = {
|
|
"task_name": "test",
|
|
# Missing service, executor, priority
|
|
}
|
|
response = client.post(
|
|
"/tasks",
|
|
headers=auth_headers,
|
|
json=incomplete_task
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
def test_create_task_with_valid_data(self, client: TestClient, auth_headers: dict, sample_task_data: dict, override_task_executor: MagicMock):
|
|
"""Test creating a task with valid data."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
|
|
# Setup mock to return task data
|
|
mock_cursor.fetchone.return_value = {**sample_task_data, "id": 1}
|
|
|
|
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",
|
|
headers=auth_headers,
|
|
json=sample_task_data
|
|
)
|
|
|
|
# Verify the call was made
|
|
assert mock_cursor.execute.called
|
|
# Check that config was JSON-encoded
|
|
call_args = mock_cursor.execute.call_args
|
|
assert 'config' in call_args[0][1]
|
|
|
|
def test_trigger_task_endpoint(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test manually triggering a task."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
|
|
# Mock task retrieval
|
|
mock_cursor.fetchone.return_value = {
|
|
"task_name": "test_task",
|
|
"enabled": True,
|
|
"priority": 50,
|
|
"executor": "example_executor"
|
|
}
|
|
|
|
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)
|
|
|
|
with patch('asyncio.create_task'):
|
|
response = client.post(
|
|
"/tasks/test_task/trigger",
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Should return success message
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert data["task_name"] == "test_task"
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestStatsEndpoint:
|
|
"""Tests for /stats endpoint."""
|
|
|
|
def test_stats_endpoint_requires_auth(self, client: TestClient):
|
|
"""Test that stats endpoint requires authentication."""
|
|
response = client.get("/stats")
|
|
assert response.status_code == 401
|
|
|
|
def test_stats_endpoint_returns_metrics(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test that stats endpoint returns system metrics."""
|
|
# Note: `/stats` also depends on get_scheduler via Depends(), which this
|
|
# test does not override (out of this fix's measured scope — see
|
|
# override_task_executor's docstring for why patch() cannot reach it).
|
|
# It is not load-bearing here: the app's real scheduler is running by
|
|
# the time TestClient's lifespan completes, so `sched.running` is True
|
|
# without an override, same as test_health_endpoint_returns_healthy.
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchone.side_effect = [
|
|
{"count": 3}, # enabled tasks
|
|
{"count": 0}, # running tasks
|
|
]
|
|
mock_cursor.fetchall.return_value = [
|
|
{"status": "success", "count": 10},
|
|
{"status": "failed", "count": 1}
|
|
]
|
|
|
|
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("/stats", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert "scheduler_running" in data
|
|
assert "tasks_enabled" in data
|
|
assert "concurrent_limit" in data
|
|
|
|
|
|
@pytest.mark.api
|
|
@pytest.mark.unit
|
|
class TestExecutionHistoryEndpoint:
|
|
"""Tests for /executions endpoint."""
|
|
|
|
def test_executions_endpoint_returns_history(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test that executions endpoint returns execution history."""
|
|
mock_conn = MagicMock()
|
|
mock_cursor = MagicMock()
|
|
mock_cursor.fetchall.return_value = [
|
|
{
|
|
"id": 1,
|
|
"task_name": "test_task",
|
|
"status": "success",
|
|
"duration_seconds": 5
|
|
}
|
|
]
|
|
|
|
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", headers=auth_headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
assert "executions" in data
|
|
assert "count" in data
|
|
|
|
def test_executions_filter_by_task_name(self, client: TestClient, auth_headers: dict, override_task_executor: MagicMock):
|
|
"""Test filtering executions by task name."""
|
|
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?task_name=test_task&limit=10",
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Should execute query with filters
|
|
assert mock_cursor.execute.called
|