Disable e2e tests by default and fix mypy errors

- Add --e2e flag to pytest to run e2e tests
- E2E tests skip by default with clear message
- Fix type annotations in webhook.py for mypy compliance
- Add proper type hints for comparison operations
This commit is contained in:
Claude
2025-12-03 16:37:14 +00:00
parent 1588f7bc71
commit e57fe7a2d8
4 changed files with 82 additions and 22 deletions
+24 -14
View File
@@ -76,7 +76,7 @@ class WebhookConfig:
# Parse expression: $.path operator value
# Supports: ==, !=, >, <, >=, <=, exists, not exists
# Note: >= and <= must come before > and < in the alternation
pattern = r'^\$\.([a-zA-Z0-9_.]+)\s+(==|!=|>=|<=|>|<|exists|not exists)\s*(.*)$'
pattern = r"^\$\.([a-zA-Z0-9_.]+)\s+(==|!=|>=|<=|>|<|exists|not exists)\s*(.*)$"
match = re.match(pattern, expr)
if not match:
@@ -88,8 +88,8 @@ class WebhookConfig:
value_str = match.group(3).strip() if match.group(3) else None
# Navigate the path
current = payload
for part in path.split('.'):
current: Any = payload
for part in path.split("."):
if isinstance(current, dict) and part in current:
current = current[part]
else:
@@ -109,8 +109,9 @@ class WebhookConfig:
return False
# Handle quoted strings
compare_value: Any
if value_str.startswith('"') and value_str.endswith('"'):
compare_value: Any = value_str[1:-1]
compare_value = value_str[1:-1]
elif value_str.startswith("'") and value_str.endswith("'"):
compare_value = value_str[1:-1]
elif value_str == "null":
@@ -131,17 +132,17 @@ class WebhookConfig:
# Perform comparison
try:
if operator == "==":
return current == compare_value
return bool(current == compare_value)
elif operator == "!=":
return current != compare_value
return bool(current != compare_value)
elif operator == ">":
return current > compare_value
return bool(current > compare_value)
elif operator == "<":
return current < compare_value
return bool(current < compare_value)
elif operator == ">=":
return current >= compare_value
return bool(current >= compare_value)
elif operator == "<=":
return current <= compare_value
return bool(current <= compare_value)
except TypeError:
return False
@@ -250,14 +251,21 @@ class WebhookDispatcher:
if tasks:
task_results = await asyncio.gather(*tasks, return_exceptions=True)
for webhook, result in zip(
[w for w in self.webhooks if w.enabled and w.matches_event(event_type, payload)],
[
w
for w in self.webhooks
if w.enabled and w.matches_event(event_type, payload)
],
task_results,
):
if isinstance(result, Exception):
results[webhook.name] = False
logger.error(f"Webhook {webhook.name} failed: {result}")
else:
elif isinstance(result, bool):
results[webhook.name] = result
else:
# Should not happen, but handle gracefully
results[webhook.name] = False
return results
@@ -319,7 +327,7 @@ class WebhookDispatcher:
# Retry with backoff (but not after the last attempt)
if attempt < webhook.max_retries:
backoff = webhook.retry_backoff * (2 ** attempt)
backoff = webhook.retry_backoff * (2**attempt)
logger.info(
f"Retrying webhook {webhook.name} in {backoff}s "
f"(attempt {attempt + 2}/{webhook.max_retries + 1})"
@@ -387,7 +395,9 @@ def create_webhook_dispatcher_from_config(
# Synchronous wrapper for use in non-async handlers
_dispatcher: Optional[WebhookDispatcher] = None
_dispatch_queue: list[tuple[str, dict[str, Any], Optional[str]]] = []
_dispatch_callback: Optional[Callable[[str, dict[str, Any], Optional[str]], None]] = None
_dispatch_callback: Optional[Callable[[str, dict[str, Any], Optional[str]], None]] = (
None
)
def set_dispatch_callback(
+6 -2
View File
@@ -33,7 +33,9 @@ class HealthStatus:
healthy: bool
component: str
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
timestamp: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
details: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
@@ -153,7 +155,9 @@ def read_health_status(component: str) -> Optional[HealthStatus]:
return None
def check_health(component: str, stale_threshold: int = HEALTH_STALE_THRESHOLD) -> tuple[bool, str]:
def check_health(
component: str, stale_threshold: int = HEALTH_STALE_THRESHOLD
) -> tuple[bool, str]:
"""Check health status for a component.
Args:
+49 -1
View File
@@ -1,4 +1,14 @@
"""Fixtures for end-to-end tests."""
"""Fixtures for end-to-end tests.
These tests require Docker Compose services to be running.
They are disabled by default and can be run with:
pytest -m e2e tests/e2e/
Or with the --e2e flag:
pytest --e2e tests/e2e/
"""
import os
import time
@@ -7,6 +17,44 @@ from typing import Generator
import httpx
import pytest
def pytest_configure(config: pytest.Config) -> None:
"""Register the e2e marker."""
config.addinivalue_line(
"markers",
"e2e: mark test as end-to-end test requiring Docker services",
)
def pytest_collection_modifyitems(
config: pytest.Config, items: list[pytest.Item]
) -> None:
"""Auto-mark all tests in this directory as e2e and skip if --e2e not provided."""
# Check if e2e tests should run
run_e2e = config.getoption("--e2e", default=False)
skip_e2e = pytest.mark.skip(
reason="E2E tests disabled by default. Use --e2e to run them."
)
for item in items:
# Mark all tests in e2e directory
if "e2e" in str(item.fspath):
item.add_marker(pytest.mark.e2e)
if not run_e2e:
item.add_marker(skip_e2e)
def pytest_addoption(parser: pytest.Parser) -> None:
"""Add --e2e option to pytest."""
parser.addoption(
"--e2e",
action="store_true",
default=False,
help="Run end-to-end tests (requires Docker services)",
)
# E2E test configuration
E2E_API_URL = os.environ.get("E2E_API_URL", "http://localhost:18000")
E2E_WEB_URL = os.environ.get("E2E_WEB_URL", "http://localhost:18080")
+3 -5
View File
@@ -1,6 +1,6 @@
"""Tests for the webhook dispatcher module."""
import asyncio
from typing import Any
from unittest.mock import AsyncMock, patch
import httpx
@@ -312,9 +312,7 @@ class TestWebhookDispatcher:
mock_response = AsyncMock()
mock_response.status_code = 500
with patch.object(
dispatcher._client, "post", return_value=mock_response
):
with patch.object(dispatcher._client, "post", return_value=mock_response):
result = await dispatcher.dispatch("event", {"data": "test"})
assert result == {"error-webhook": False}
@@ -403,7 +401,7 @@ class TestWebhookDispatcherFactory:
def test_create_from_config(self):
"""Test creating dispatcher from configuration."""
config = [
config: list[dict[str, Any]] = [
{
"name": "webhook-1",
"url": "https://example.com/webhook1",