Files
scheduler/tests/test_integration.py
T
jpmschweitzer 3f987bcf64 fix(tests): repair test_create_list_delete_task_flow's two defects
Same dependency_overrides fix as the other test_*.py commits for
get_task_executor (patch() cannot reach a route's already-registered
Depends() reference — see the conftest.py commit for the full explanation
and the empirical check that established it).

Once the mock actually intercepted the DB layer, a second, independent
defect surfaced: KeyError: 0. mock_cursor.fetchone.side_effect held 3 values
sized for "create, list, delete" (one fetchone() each), but list_tasks uses
fetchall(), not fetchone(), and delete_task has grown a second fetchone()
call since this test was written — c34db66 added the 409-on-history-loss
check, which does `row = cur.fetchone()` for the task id and then a
separate `cur.fetchone()[0]` for its execution count. A real create->list->
delete flow through this endpoint set needs 1 (create) + 2 (delete) = 3
fetchone() values in that order, not one per named step. Re-sequenced so
delete_task sees a real id and a zero execution count, reaching the intended
200 rather than an unhandled KeyError.

Source unchanged; delete_task's behavior is deliberate (c34db66) and this
integration test had not been updated to track it.
2026-08-18 15:50:29 +02:00

196 lines
7.0 KiB
Python

"""
Integration tests with database.
These tests can be run with a test database or skipped if not available.
"""
import pytest
import os
from fastapi.testclient import TestClient
from unittest.mock import patch
@pytest.mark.integration
@pytest.mark.skipif(
os.getenv("RUN_INTEGRATION_TESTS") != "true",
reason="Integration tests require RUN_INTEGRATION_TESTS=true"
)
class TestDatabaseIntegration:
"""Integration tests that use actual database."""
def test_health_with_real_scheduler(self, client: TestClient):
"""Test health endpoint with real scheduler instance."""
# This test runs against the actual scheduler if it's running
# For true integration testing, we'd set up a test database
pass
@pytest.mark.integration
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, override_task_executor):
"""Test complete task lifecycle: create → list → delete."""
from unittest.mock import MagicMock
# This simulates the full flow with mocked database
mock_conn = MagicMock()
mock_cursor = MagicMock()
# 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)
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
)
# List
list_response = client.get("/tasks", 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]
@pytest.mark.integration
class TestTaskExecutorIntegration:
"""Integration tests for task execution."""
@pytest.mark.asyncio
async def test_execute_example_task_real(self, test_settings):
"""Test executing example executor with real implementation."""
from src.executors import example_executor
config = {
"message": "Integration test",
"delay_seconds": 0
}
result = await example_executor.execute(config, test_settings)
assert "Integration test" in result
assert "took" in result.lower()
@pytest.mark.asyncio
async def test_task_scheduling_logic(self, test_settings):
"""Test task scheduling logic."""
from src.tasks.executor import TaskExecutor
from datetime import datetime
executor = TaskExecutor(test_settings)
# Test various scheduling scenarios
task_every_minute = {
'minute': -1, 'hour': -1, 'day_of_month': -1,
'month': -1, 'day_of_week': -1
}
task_specific_time = {
'minute': 30, 'hour': 14, 'day_of_month': -1,
'month': -1, 'day_of_week': -1
}
now = datetime(2025, 12, 7, 14, 30, 0)
# Every minute task should always run
assert executor._should_run_now(task_every_minute, now) is True
# Specific time task should run at 14:30
assert executor._should_run_now(task_specific_time, now) is True
# But not at 14:31
now_plus_one = datetime(2025, 12, 7, 14, 31, 0)
assert executor._should_run_now(task_specific_time, now_plus_one) is False
@pytest.mark.integration
class TestErrorHandling:
"""Integration tests for error handling."""
@pytest.mark.asyncio
async def test_executor_with_invalid_module(self, test_settings, sample_task_data: dict):
"""Test task execution with invalid executor module."""
from src.tasks.executor import TaskExecutor
executor = TaskExecutor(test_settings)
task = {**sample_task_data, "executor": "nonexistent_executor"}
with patch.object(executor, 'get_db_connection') as mock_get_conn:
from unittest.mock import MagicMock
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor)
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=None)
mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn)
mock_get_conn.return_value.__exit__ = MagicMock(return_value=None)
# Should handle the error gracefully
await executor.execute_task(task)
# Should have recorded the error
assert mock_cursor.execute.called
@pytest.mark.integration
class TestConfigValidation:
"""Integration tests for configuration validation."""
def test_settings_loads_from_environment(self):
"""Test that settings load correctly from environment."""
from src.config import get_settings
settings = get_settings()
# Should have loaded test environment variables
assert settings.postgres_host == "test-postgres"
assert settings.postgres_db == "test_scheduler"
assert settings.scheduler_api_key == "test-api-key-12345"
def test_settings_provides_database_url(self):
"""Test that settings provides correct database URL."""
from src.config import get_settings
settings = get_settings()
db_url = settings.database_url
assert "postgresql://" in db_url
assert "test_scheduler" in db_url
def test_settings_provides_redis_url(self):
"""Test that settings provides correct Redis URL."""
from src.config import get_settings
settings = get_settings()
redis_url = settings.redis_url
assert "redis://" in redis_url