diff --git a/src/meshcore_hub/collector/webhook.py b/src/meshcore_hub/collector/webhook.py index 7153e4b..ac6e28f 100644 --- a/src/meshcore_hub/collector/webhook.py +++ b/src/meshcore_hub/collector/webhook.py @@ -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( diff --git a/src/meshcore_hub/common/health.py b/src/meshcore_hub/common/health.py index de0558d..fd40fb2 100644 --- a/src/meshcore_hub/common/health.py +++ b/src/meshcore_hub/common/health.py @@ -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: diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 678f47b..459d502 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -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") diff --git a/tests/test_collector/test_webhook.py b/tests/test_collector/test_webhook.py index c4c29aa..11a87c2 100644 --- a/tests/test_collector/test_webhook.py +++ b/tests/test_collector/test_webhook.py @@ -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",