| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789 |
- # -*- coding: utf-8 -*-
- """
- Tests für Network Security Module.
- Testet Access Control, Rate Limiting und DDoS Protection.
- """
- import asyncio
- import time
- import pytest
- from datetime import datetime, timedelta
- from trixy_core.network.security.access_control import (
- AccessController,
- AccessRule,
- AccessDecision,
- IPMatcher,
- CIDRMatcher,
- MACMatcher,
- WildcardMatcher,
- )
- from trixy_core.network.security.rate_limiter import (
- ConnectionRateLimiter,
- RateLimitConfig,
- RateLimitResult,
- IPRateLimiter,
- GlobalRateLimiter,
- TokenBucket,
- )
- from trixy_core.network.security.ddos import (
- DDoSProtection,
- DDoSConfig,
- ThreatLevel,
- ConnectionTracker,
- SuspiciousActivityDetector,
- )
- # =============================================================================
- # Access Control Tests
- # =============================================================================
- class TestIPMatcher:
- """Tests für IPMatcher."""
- def test_ipv4_exact_match(self):
- """IPv4 exakter Match."""
- matcher = IPMatcher("192.168.1.100")
- assert matcher.matches("192.168.1.100") is True
- assert matcher.matches("192.168.1.101") is False
- def test_ipv6_exact_match(self):
- """IPv6 exakter Match."""
- matcher = IPMatcher("::1")
- assert matcher.matches("::1") is True
- assert matcher.matches("::2") is False
- def test_invalid_ip_no_match(self):
- """Ungültige IP matcht nicht."""
- matcher = IPMatcher("192.168.1.100")
- assert matcher.matches("invalid") is False
- assert matcher.matches("") is False
- class TestCIDRMatcher:
- """Tests für CIDRMatcher."""
- def test_cidr_in_range(self):
- """IP im CIDR-Bereich."""
- matcher = CIDRMatcher("192.168.1.0/24")
- assert matcher.matches("192.168.1.0") is True
- assert matcher.matches("192.168.1.100") is True
- assert matcher.matches("192.168.1.255") is True
- def test_cidr_out_of_range(self):
- """IP außerhalb des CIDR-Bereichs."""
- matcher = CIDRMatcher("192.168.1.0/24")
- assert matcher.matches("192.168.2.1") is False
- assert matcher.matches("10.0.0.1") is False
- def test_cidr_slash_32(self):
- """CIDR /32 = einzelne IP."""
- matcher = CIDRMatcher("192.168.1.100/32")
- assert matcher.matches("192.168.1.100") is True
- assert matcher.matches("192.168.1.101") is False
- def test_cidr_slash_16(self):
- """CIDR /16 = Class B Netzwerk."""
- matcher = CIDRMatcher("10.0.0.0/16")
- assert matcher.matches("10.0.0.1") is True
- assert matcher.matches("10.0.255.255") is True
- assert matcher.matches("10.1.0.1") is False
- class TestMACMatcher:
- """Tests für MACMatcher."""
- def test_mac_colon_format(self):
- """MAC mit Doppelpunkt-Format."""
- matcher = MACMatcher("AA:BB:CC:DD:EE:FF")
- assert matcher.matches("AA:BB:CC:DD:EE:FF") is True
- assert matcher.matches("aa:bb:cc:dd:ee:ff") is True # Case-insensitive
- def test_mac_dash_format(self):
- """MAC mit Bindestrich-Format."""
- matcher = MACMatcher("AA-BB-CC-DD-EE-FF")
- assert matcher.matches("AA:BB:CC:DD:EE:FF") is True
- def test_mac_no_separator(self):
- """MAC ohne Trennzeichen."""
- matcher = MACMatcher("AABBCCDDEEFF")
- assert matcher.matches("AA:BB:CC:DD:EE:FF") is True
- def test_mac_no_match(self):
- """MAC matcht nicht."""
- matcher = MACMatcher("AA:BB:CC:DD:EE:FF")
- assert matcher.matches("11:22:33:44:55:66") is False
- class TestWildcardMatcher:
- """Tests für WildcardMatcher."""
- def test_wildcard_single_segment(self):
- """Wildcard für ein Segment."""
- matcher = WildcardMatcher("192.168.1.*")
- assert matcher.matches("192.168.1.0") is True
- assert matcher.matches("192.168.1.255") is True
- assert matcher.matches("192.168.2.1") is False
- def test_wildcard_multiple_segments(self):
- """Wildcard für mehrere Segmente."""
- matcher = WildcardMatcher("192.168.*.*")
- assert matcher.matches("192.168.0.1") is True
- assert matcher.matches("192.168.255.255") is True
- assert matcher.matches("192.169.0.1") is False
- def test_wildcard_prefix(self):
- """Wildcard am Anfang."""
- matcher = WildcardMatcher("*.168.1.100")
- assert matcher.matches("192.168.1.100") is True
- assert matcher.matches("10.168.1.100") is True
- class TestAccessRule:
- """Tests für AccessRule."""
- def test_rule_creation(self):
- """Regel erstellen."""
- rule = AccessRule(
- pattern="192.168.1.0/24",
- decision=AccessDecision.ALLOW,
- description="Lokales Netzwerk"
- )
- assert rule.pattern == "192.168.1.0/24"
- assert rule.decision == AccessDecision.ALLOW
- assert rule.hit_count == 0
- def test_rule_expiry(self):
- """Regel-Ablauf prüfen."""
- # Nicht abgelaufene Regel
- rule = AccessRule(
- pattern="test",
- decision=AccessDecision.DENY,
- expires_at=datetime.now() + timedelta(hours=1)
- )
- assert rule.is_expired() is False
- # Abgelaufene Regel
- rule_expired = AccessRule(
- pattern="test",
- decision=AccessDecision.DENY,
- expires_at=datetime.now() - timedelta(hours=1)
- )
- assert rule_expired.is_expired() is True
- def test_rule_hit_count(self):
- """Treffer zählen."""
- rule = AccessRule(pattern="test", decision=AccessDecision.ALLOW)
- assert rule.hit_count == 0
- rule.record_hit()
- assert rule.hit_count == 1
- rule.record_hit()
- assert rule.hit_count == 2
- class TestAccessController:
- """Tests für AccessController."""
- def test_default_deny(self):
- """Standard: Deny."""
- controller = AccessController(default_allow=False)
- assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
- def test_default_allow(self):
- """Standard: Allow."""
- controller = AccessController(default_allow=True)
- assert controller.check_ip("192.168.1.100") == AccessDecision.ALLOW
- def test_whitelist_ip(self):
- """Whitelist einzelne IP."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.100", "Erlaubte IP")
- assert controller.check_ip("192.168.1.100") == AccessDecision.ALLOW
- assert controller.check_ip("192.168.1.101") == AccessDecision.DENY
- def test_whitelist_cidr(self):
- """Whitelist CIDR-Bereich."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.0/24", "Lokales Netzwerk")
- assert controller.check_ip("192.168.1.50") == AccessDecision.ALLOW
- assert controller.check_ip("192.168.2.50") == AccessDecision.DENY
- def test_blacklist_overrides_whitelist(self):
- """Blacklist hat Vorrang vor Whitelist."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.0/24", "Netzwerk")
- controller.blacklist("192.168.1.100", "Gesperrte IP")
- assert controller.check_ip("192.168.1.50") == AccessDecision.ALLOW
- assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
- def test_mac_whitelist(self):
- """MAC-Whitelist."""
- controller = AccessController(default_allow=False)
- controller.whitelist_mac("AA:BB:CC:DD:EE:FF", "Erlaubtes Gerät")
- assert controller.check_mac("AA:BB:CC:DD:EE:FF") == AccessDecision.ALLOW
- assert controller.check_mac("11:22:33:44:55:66") == AccessDecision.DENY
- def test_combined_check(self):
- """Kombinierte IP + MAC Prüfung."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.0/24")
- controller.whitelist_mac("AA:BB:CC:DD:EE:FF")
- # Beide erlaubt
- assert controller.check("192.168.1.100", "AA:BB:CC:DD:EE:FF") == AccessDecision.ALLOW
- # IP erlaubt, MAC nicht
- assert controller.check("192.168.1.100", "11:22:33:44:55:66") == AccessDecision.DENY
- # IP nicht erlaubt
- assert controller.check("10.0.0.1", "AA:BB:CC:DD:EE:FF") == AccessDecision.DENY
- def test_expired_rules_ignored(self):
- """Abgelaufene Regeln werden ignoriert."""
- controller = AccessController(default_allow=False)
- controller.whitelist(
- "192.168.1.100",
- expires_at=datetime.now() - timedelta(hours=1)
- )
- assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
- def test_remove_from_whitelist(self):
- """Regel von Whitelist entfernen."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.100")
- assert controller.check_ip("192.168.1.100") == AccessDecision.ALLOW
- controller.remove_from_whitelist("192.168.1.100")
- assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
- def test_clear_expired(self):
- """Abgelaufene Regeln bereinigen."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.1", expires_at=datetime.now() - timedelta(hours=1))
- controller.whitelist("192.168.1.2", expires_at=datetime.now() + timedelta(hours=1))
- controller.whitelist("192.168.1.3") # Kein Ablauf
- count = controller.clear_expired()
- assert count == 1
- def test_statistics(self):
- """Statistiken abrufen."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.0/24")
- controller.blacklist("10.0.0.0/8")
- stats = controller.get_statistics()
- assert stats["whitelist_count"] == 1
- assert stats["blacklist_count"] == 1
- def test_serialization(self):
- """Serialisierung und Deserialisierung."""
- controller = AccessController(default_allow=False)
- controller.whitelist("192.168.1.0/24", "Netzwerk")
- controller.blacklist("10.0.0.1", "Gesperrt")
- data = controller.to_dict()
- restored = AccessController.from_dict(data)
- assert restored.check_ip("192.168.1.100") == AccessDecision.ALLOW
- assert restored.check_ip("10.0.0.1") == AccessDecision.DENY
- # =============================================================================
- # Rate Limiter Tests
- # =============================================================================
- class TestTokenBucket:
- """Tests für TokenBucket."""
- def test_initial_capacity(self):
- """Initiale Kapazität."""
- bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=1.0)
- assert bucket.tokens == 10.0
- def test_consume_tokens(self):
- """Tokens verbrauchen."""
- bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=1.0)
- assert bucket.consume(5) is True
- # Tokens können durch Refill leicht höher sein
- assert 4.5 <= bucket.tokens <= 5.5
- assert bucket.consume(5) is True
- # Fast 0, aber Refill kann kleine Menge hinzufügen
- assert bucket.tokens < 0.5
- assert bucket.consume(1) is False # Nicht genug Tokens
- def test_refill(self):
- """Token-Refill."""
- bucket = TokenBucket(capacity=10, tokens=0.0, refill_rate=100.0)
- time.sleep(0.05) # 50ms = 5 Tokens bei 100/s
- bucket.refill()
- assert bucket.tokens >= 4.0 # Mindestens 4 Tokens
- def test_capacity_limit(self):
- """Kapazitätslimit."""
- bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=100.0)
- time.sleep(0.1)
- bucket.refill()
- assert bucket.tokens == 10.0 # Nicht über Kapazität
- def test_blocking(self):
- """Bucket blockieren."""
- bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=1.0)
- assert bucket.is_blocked() is False
- bucket.block(1.0)
- assert bucket.is_blocked() is True
- time.sleep(1.1)
- assert bucket.is_blocked() is False
- class TestIPRateLimiter:
- """Tests für IPRateLimiter."""
- def test_allow_under_limit(self):
- """Erlauben unter Limit."""
- config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
- limiter = IPRateLimiter(config)
- for _ in range(5):
- result = limiter.check("192.168.1.100")
- assert result == RateLimitResult.ALLOWED
- def test_limit_exceeded(self):
- """Limit überschritten."""
- config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
- limiter = IPRateLimiter(config)
- assert limiter.check("192.168.1.100") == RateLimitResult.ALLOWED
- assert limiter.check("192.168.1.100") == RateLimitResult.ALLOWED
- assert limiter.check("192.168.1.100") == RateLimitResult.LIMITED
- def test_different_ips_separate_limits(self):
- """Verschiedene IPs haben separate Limits."""
- config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
- limiter = IPRateLimiter(config)
- # IP 1 ausschöpfen
- limiter.check("192.168.1.1")
- limiter.check("192.168.1.1")
- assert limiter.check("192.168.1.1") == RateLimitResult.LIMITED
- # IP 2 noch erlaubt
- assert limiter.check("192.168.1.2") == RateLimitResult.ALLOWED
- def test_blocking_after_threshold(self):
- """Blockierung nach Schwellenwert."""
- config = RateLimitConfig(
- requests_per_second=1.0,
- burst_size=1,
- block_threshold=3,
- block_duration=10.0
- )
- limiter = IPRateLimiter(config)
- # Erste Anfrage OK
- limiter.check("192.168.1.100")
- # Violations sammeln
- for _ in range(3):
- limiter.check("192.168.1.100")
- # Sollte jetzt blockiert sein
- result = limiter.check("192.168.1.100")
- assert result == RateLimitResult.BLOCKED
- def test_unblock_ip(self):
- """IP entsperren."""
- config = RateLimitConfig(
- requests_per_second=1.0,
- burst_size=1,
- block_threshold=2,
- block_duration=10.0
- )
- limiter = IPRateLimiter(config)
- # Blockieren
- limiter.check("192.168.1.100")
- limiter.check("192.168.1.100")
- limiter.check("192.168.1.100")
- assert limiter.check("192.168.1.100") == RateLimitResult.BLOCKED
- # Entsperren
- limiter.unblock("192.168.1.100")
- assert limiter.check("192.168.1.100") == RateLimitResult.ALLOWED
- def test_get_status(self):
- """Status abrufen."""
- config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
- limiter = IPRateLimiter(config)
- limiter.check("192.168.1.100")
- limiter.check("192.168.1.100")
- status = limiter.get_status("192.168.1.100")
- assert status["exists"] is True
- assert status["tokens"] < 5.0
- def test_reset(self):
- """Reset einer IP."""
- config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
- limiter = IPRateLimiter(config)
- limiter.check("192.168.1.100")
- limiter.check("192.168.1.100")
- limiter.reset("192.168.1.100")
- status = limiter.get_status("192.168.1.100")
- assert status["exists"] is False
- def test_cleanup(self):
- """Cleanup inaktiver Buckets."""
- config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
- limiter = IPRateLimiter(config)
- limiter.check("192.168.1.100")
- # Bucket sollte existieren
- assert limiter.get_status("192.168.1.100")["exists"] is True
- # Cleanup (wird normalerweise nach Timeout aufgerufen)
- count = limiter.cleanup()
- # Bei kurzer Inaktivität wird nichts entfernt
- assert count == 0
- class TestGlobalRateLimiter:
- """Tests für GlobalRateLimiter."""
- def test_allow_under_limit(self):
- """Erlauben unter globalem Limit."""
- limiter = GlobalRateLimiter(max_requests_per_second=100.0, burst_size=10)
- for _ in range(10):
- result = limiter.check()
- assert result == RateLimitResult.ALLOWED
- def test_limit_exceeded(self):
- """Globales Limit überschritten."""
- limiter = GlobalRateLimiter(max_requests_per_second=1.0, burst_size=3)
- limiter.check()
- limiter.check()
- limiter.check()
- assert limiter.check() == RateLimitResult.LIMITED
- def test_get_status(self):
- """Status abrufen."""
- limiter = GlobalRateLimiter(max_requests_per_second=10.0, burst_size=10)
- limiter.check()
- limiter.check()
- status = limiter.get_status()
- assert status["total_requests"] == 2
- assert status["tokens_available"] < 10.0
- class TestConnectionRateLimiter:
- """Tests für kombinierten Rate-Limiter."""
- def test_combined_limits(self):
- """Kombinierte IP- und Global-Limits."""
- ip_config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
- limiter = ConnectionRateLimiter(
- ip_config=ip_config,
- global_rps=100.0,
- global_burst=50
- )
- # Unter beiden Limits
- result = limiter.check("192.168.1.100")
- assert result == RateLimitResult.ALLOWED
- def test_ip_limit_triggers_first(self):
- """IP-Limit greift zuerst."""
- ip_config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
- limiter = ConnectionRateLimiter(
- ip_config=ip_config,
- global_rps=1000.0,
- global_burst=1000
- )
- limiter.check("192.168.1.100")
- limiter.check("192.168.1.100")
- result = limiter.check("192.168.1.100")
- assert result == RateLimitResult.LIMITED
- def test_statistics(self):
- """Kombinierte Statistiken."""
- ip_config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
- limiter = ConnectionRateLimiter(ip_config=ip_config)
- limiter.check("192.168.1.100")
- stats = limiter.get_statistics()
- assert "ip" in stats
- assert "global" in stats
- # =============================================================================
- # DDoS Protection Tests
- # =============================================================================
- class TestConnectionTracker:
- """Tests für ConnectionTracker."""
- def test_register_connection(self):
- """Verbindung registrieren."""
- config = DDoSConfig(max_connections_per_ip=5)
- tracker = ConnectionTracker(config)
- conn_id, allowed = tracker.register_connection("192.168.1.100")
- assert allowed is True
- assert conn_id != ""
- def test_max_connections_per_ip(self):
- """Maximale Verbindungen pro IP."""
- config = DDoSConfig(max_connections_per_ip=2)
- tracker = ConnectionTracker(config)
- conn1, allowed1 = tracker.register_connection("192.168.1.100")
- conn2, allowed2 = tracker.register_connection("192.168.1.100")
- _, allowed3 = tracker.register_connection("192.168.1.100")
- assert allowed1 is True
- assert allowed2 is True
- assert allowed3 is False
- def test_unregister_connection(self):
- """Verbindung entfernen."""
- config = DDoSConfig(max_connections_per_ip=2)
- tracker = ConnectionTracker(config)
- conn_id, _ = tracker.register_connection("192.168.1.100")
- tracker.register_connection("192.168.1.100")
- assert tracker.get_connection_count("192.168.1.100") == 2
- tracker.unregister_connection(conn_id)
- assert tracker.get_connection_count("192.168.1.100") == 1
- def test_ban_ip(self):
- """IP sperren."""
- config = DDoSConfig()
- tracker = ConnectionTracker(config)
- tracker.ban_ip("192.168.1.100", duration=10.0)
- assert tracker.is_banned("192.168.1.100") is True
- _, allowed = tracker.register_connection("192.168.1.100")
- assert allowed is False
- def test_unban_ip(self):
- """IP entsperren."""
- config = DDoSConfig()
- tracker = ConnectionTracker(config)
- tracker.ban_ip("192.168.1.100")
- assert tracker.is_banned("192.168.1.100") is True
- tracker.unban_ip("192.168.1.100")
- assert tracker.is_banned("192.168.1.100") is False
- def test_global_connection_limit(self):
- """Globales Verbindungslimit."""
- config = DDoSConfig(max_global_connections=3)
- tracker = ConnectionTracker(config)
- tracker.register_connection("192.168.1.1")
- tracker.register_connection("192.168.1.2")
- tracker.register_connection("192.168.1.3")
- _, allowed = tracker.register_connection("192.168.1.4")
- assert allowed is False
- def test_statistics(self):
- """Statistiken abrufen."""
- config = DDoSConfig()
- tracker = ConnectionTracker(config)
- tracker.register_connection("192.168.1.100")
- tracker.register_connection("192.168.1.101")
- stats = tracker.get_statistics()
- assert stats["total_connections"] == 2
- assert stats["unique_ips"] == 2
- class TestSuspiciousActivityDetector:
- """Tests für SuspiciousActivityDetector."""
- def test_record_event(self):
- """Event aufzeichnen."""
- config = DDoSConfig(monitoring_window=60.0)
- detector = SuspiciousActivityDetector(config)
- detector.record_event("192.168.1.100", "connection_denied")
- level = detector.analyze("192.168.1.100")
- assert level == ThreatLevel.LOW
- def test_threat_level_escalation(self):
- """Bedrohungsstufe eskaliert."""
- config = DDoSConfig(monitoring_window=60.0)
- detector = SuspiciousActivityDetector(config)
- # Viele Events = höhere Bedrohung
- for _ in range(50):
- detector.record_event("192.168.1.100", "connection_denied")
- level = detector.analyze("192.168.1.100")
- assert level >= ThreatLevel.MEDIUM
- def test_get_suspicious_ips(self):
- """Verdächtige IPs abrufen."""
- config = DDoSConfig(monitoring_window=60.0)
- detector = SuspiciousActivityDetector(config)
- for _ in range(20):
- detector.record_event("192.168.1.100", "connection_denied")
- suspicious = detector.get_suspicious_ips(ThreatLevel.LOW)
- assert len(suspicious) > 0
- assert suspicious[0][0] == "192.168.1.100"
- class TestDDoSProtection:
- """Tests für DDoSProtection."""
- @pytest.mark.asyncio
- async def test_on_connect_allowed(self):
- """Verbindung erlaubt."""
- config = DDoSConfig(max_connections_per_ip=5)
- protection = DDoSProtection(config)
- conn_id, allowed = await protection.on_connect("192.168.1.100")
- assert allowed is True
- assert conn_id != ""
- @pytest.mark.asyncio
- async def test_on_connect_denied(self):
- """Verbindung verweigert."""
- config = DDoSConfig(max_connections_per_ip=1)
- protection = DDoSProtection(config)
- await protection.on_connect("192.168.1.100")
- _, allowed = await protection.on_connect("192.168.1.100")
- assert allowed is False
- @pytest.mark.asyncio
- async def test_on_disconnect(self):
- """Verbindung getrennt."""
- config = DDoSConfig(max_connections_per_ip=2)
- protection = DDoSProtection(config)
- conn_id, _ = await protection.on_connect("192.168.1.100")
- await protection.on_connect("192.168.1.100")
- await protection.on_disconnect(conn_id)
- # Jetzt sollte wieder Platz sein
- _, allowed = await protection.on_connect("192.168.1.100")
- assert allowed is True
- @pytest.mark.asyncio
- async def test_ban_unban(self):
- """IP sperren und entsperren."""
- protection = DDoSProtection()
- protection.ban_ip("192.168.1.100")
- assert protection.is_allowed("192.168.1.100") is False
- protection.unban_ip("192.168.1.100")
- assert protection.is_allowed("192.168.1.100") is True
- @pytest.mark.asyncio
- async def test_threat_callback(self):
- """Threat-Callback wird aufgerufen."""
- config = DDoSConfig(max_connections_per_ip=1)
- protection = DDoSProtection(config)
- threats_detected = []
- @protection.on_threat
- def handle_threat(ip: str, level: ThreatLevel):
- threats_detected.append((ip, level))
- # Viele fehlgeschlagene Verbindungsversuche
- await protection.on_connect("192.168.1.100")
- for _ in range(10):
- await protection.on_connect("192.168.1.100")
- # Nach vielen Versuchen sollte Callback aufgerufen werden
- # (abhängig von der Threat-Level-Logik)
- @pytest.mark.asyncio
- async def test_statistics(self):
- """Statistiken abrufen."""
- protection = DDoSProtection()
- await protection.on_connect("192.168.1.100")
- stats = protection.get_statistics()
- assert "connections" in stats
- assert "threats" in stats
- assert "config" in stats
- # =============================================================================
- # Async Tests
- # =============================================================================
- class TestAsyncRateLimiter:
- """Tests für async Rate-Limiter-Methoden."""
- @pytest.mark.asyncio
- async def test_ip_rate_limiter_async(self):
- """Async IP Rate-Limiter."""
- config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
- limiter = IPRateLimiter(config)
- result = await limiter.check_async("192.168.1.100")
- assert result == RateLimitResult.ALLOWED
- @pytest.mark.asyncio
- async def test_global_rate_limiter_async(self):
- """Async Global Rate-Limiter."""
- limiter = GlobalRateLimiter(max_requests_per_second=10.0, burst_size=5)
- result = await limiter.check_async()
- assert result == RateLimitResult.ALLOWED
- @pytest.mark.asyncio
- async def test_connection_rate_limiter_async(self):
- """Async kombinierter Rate-Limiter."""
- ip_config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
- limiter = ConnectionRateLimiter(ip_config=ip_config)
- result = await limiter.check_async("192.168.1.100")
- assert result == RateLimitResult.ALLOWED
|