Files
scheduler/tests/test_integration.py
T
jpmschweitzer 6915be31ca fix(tests): match postgres_host assertion to the fixture actually in effect
tests/conftest.py sets os.environ["POSTGRES_HOST"] = "postgres-shared" at
module level (before `from src.main import app`), commented "Use real
postgres for integration tests". test_settings_loads_from_environment
asserted settings.postgres_host == "test-postgres", a value grep confirms
nothing in this suite has ever set — git log -p shows both the conftest
line and this assertion originate in the same single commit and neither has
changed since. Matched the assertion to the environment the suite actually
runs under.

Not a source change and not a claim that "postgres-shared" is the right
fixture value for a suite this ticket also found is not actually hermetic
where that value is concerned (see the 12 pre-existing DNS errors, tracked
separately from this fix) — only that the assertion should test what the
fixture sets, not an unset value.
2026-08-18 15:50:45 +02:00

201 lines
7.4 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. tests/conftest.py
# sets POSTGRES_HOST="postgres-shared" (module-level, before `from
# src.main import app`), annotated "Use real postgres for integration
# tests" — this assertion checked for "test-postgres", a value
# nothing in the suite has ever set. Matched to the fixture actually
# in effect rather than to an unset value.
assert settings.postgres_host == "postgres-shared"
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