"""Tests for webhook routes — trigger, validation, rate limiting.""" import time from unittest.mock import AsyncMock, MagicMock, patch import pytest from wled_controller.api.routes.webhooks import _check_rate_limit, _rate_hits # --------------------------------------------------------------------------- # Rate limiter unit tests (pure function, no HTTP) # --------------------------------------------------------------------------- class TestRateLimiter: def setup_method(self): """Clear rate-limit state between tests.""" _rate_hits.clear() def test_allows_under_limit(self): for _ in range(29): _check_rate_limit("1.2.3.4") # should not raise def test_rejects_at_limit(self): for _ in range(30): _check_rate_limit("1.2.3.4") from fastapi import HTTPException with pytest.raises(HTTPException) as exc_info: _check_rate_limit("1.2.3.4") assert exc_info.value.status_code == 429 def test_separate_ips_independent(self): for _ in range(30): _check_rate_limit("10.0.0.1") # Different IP should still be allowed _check_rate_limit("10.0.0.2") # should not raise def test_window_expiry(self): """Timestamps outside the 60s window are pruned.""" old_time = time.time() - 120 # 2 minutes ago _rate_hits["1.2.3.4"] = [old_time] * 30 # Old entries should be pruned, allowing new requests _check_rate_limit("1.2.3.4") # should not raise # --------------------------------------------------------------------------- # Webhook payload validation # --------------------------------------------------------------------------- class TestWebhookPayload: def test_valid_payload_model(self): from wled_controller.api.routes.webhooks import WebhookPayload p = WebhookPayload(action="activate") assert p.action == "activate" p2 = WebhookPayload(action="deactivate") assert p2.action == "deactivate" def test_arbitrary_action_accepted_by_model(self): """The model accepts any string; validation is in the route handler.""" from wled_controller.api.routes.webhooks import WebhookPayload p = WebhookPayload(action="bogus") assert p.action == "bogus"