"""Property-based tests for the Notification Service. Feature: autonomous-trading-engine Property 30: Notification rate limiting. """ from __future__ import annotations from hypothesis import given, settings from hypothesis import strategies as st from services.trading.notifications import NotificationService # --------------------------------------------------------------------------- # Property 30: Notification rate limiting # **Validates: Requirements 19.7** # --------------------------------------------------------------------------- class TestProperty30NotificationRateLimiting: """Property 30: Notification rate limiting. Generate random sequences of notification requests within a one-hour window. Verify at most 10 SMS and 20 emails allowed per hour. Verify excess notifications blocked (should_send returns False). **Validates: Requirements 19.7** """ @settings(max_examples=100) @given( sms_limit=st.integers(min_value=1, max_value=50), email_limit=st.integers(min_value=1, max_value=50), sms_requests=st.integers(min_value=0, max_value=100), email_requests=st.integers(min_value=0, max_value=100), ) def test_sms_rate_limit_enforced( self, sms_limit: int, email_limit: int, sms_requests: int, email_requests: int, ) -> None: """At most sms_limit SMS notifications are allowed per hour.""" svc = NotificationService( sms_enabled=True, email_enabled=True, rate_limit_sms_per_hour=sms_limit, rate_limit_email_per_hour=email_limit, ) sent_sms = 0 for i in range(sms_requests): if svc.should_send("sms", current_hour_count=i): sent_sms += 1 assert sent_sms <= sms_limit @settings(max_examples=100) @given( sms_limit=st.integers(min_value=1, max_value=50), email_limit=st.integers(min_value=1, max_value=50), sms_requests=st.integers(min_value=0, max_value=100), email_requests=st.integers(min_value=0, max_value=100), ) def test_email_rate_limit_enforced( self, sms_limit: int, email_limit: int, sms_requests: int, email_requests: int, ) -> None: """At most email_limit email notifications are allowed per hour.""" svc = NotificationService( sms_enabled=True, email_enabled=True, rate_limit_sms_per_hour=sms_limit, rate_limit_email_per_hour=email_limit, ) sent_email = 0 for i in range(email_requests): if svc.should_send("email", current_hour_count=i): sent_email += 1 assert sent_email <= email_limit @settings(max_examples=100) @given( sms_limit=st.integers(min_value=1, max_value=50), email_limit=st.integers(min_value=1, max_value=50), ) def test_excess_sms_blocked( self, sms_limit: int, email_limit: int, ) -> None: """Notifications beyond the limit are blocked.""" svc = NotificationService( sms_enabled=True, email_enabled=True, rate_limit_sms_per_hour=sms_limit, rate_limit_email_per_hour=email_limit, ) # At the limit, should_send returns False assert svc.should_send("sms", current_hour_count=sms_limit) is False # One past the limit, still False assert svc.should_send("sms", current_hour_count=sms_limit + 1) is False @settings(max_examples=100) @given( sms_limit=st.integers(min_value=1, max_value=50), email_limit=st.integers(min_value=1, max_value=50), ) def test_excess_email_blocked( self, sms_limit: int, email_limit: int, ) -> None: """Email notifications beyond the limit are blocked.""" svc = NotificationService( sms_enabled=True, email_enabled=True, rate_limit_sms_per_hour=sms_limit, rate_limit_email_per_hour=email_limit, ) assert svc.should_send("email", current_hour_count=email_limit) is False assert svc.should_send("email", current_hour_count=email_limit + 1) is False @settings(max_examples=100) @given( count=st.integers(min_value=0, max_value=100), ) def test_default_limits_10_sms_20_email( self, count: int, ) -> None: """Default limits are 10 SMS and 20 emails per hour.""" svc = NotificationService(sms_enabled=True, email_enabled=True) sms_allowed = svc.should_send("sms", current_hour_count=count) email_allowed = svc.should_send("email", current_hour_count=count) if count < 10: assert sms_allowed is True else: assert sms_allowed is False if count < 20: assert email_allowed is True else: assert email_allowed is False @settings(max_examples=100) @given( count=st.integers(min_value=0, max_value=50), ) def test_disabled_channel_always_blocked( self, count: int, ) -> None: """Disabled channels always return False regardless of count.""" svc = NotificationService(sms_enabled=False, email_enabled=False) assert svc.should_send("sms", current_hour_count=count) is False assert svc.should_send("email", current_hour_count=count) is False