Features: - Core moderation: warn, kick, ban, timeout, strike system - Automod: banned words filter, scam detection, anti-spam, link filtering - AI moderation: Claude/OpenAI integration, NSFW detection, phishing analysis - Verification system: button, captcha, math, emoji challenges - Rate limiting system with configurable scopes - Event logging: joins, leaves, message edits/deletes, voice activity - Per-guild configuration with caching - Docker deployment support Bug fixes applied: - Fixed await on session.delete() in guild_config.py - Fixed memory leak in AI moderation message tracking (use deque) - Added error handling to bot shutdown - Added error handling to timeout command - Removed unused Literal import - Added prefix validation - Added image analysis limit (3 per message) - Fixed test mock for SQLAlchemy model
143 lines
4.9 KiB
Python
143 lines
4.9 KiB
Python
"""Tests for verification service."""
|
|
|
|
import pytest
|
|
|
|
from guardden.services.verification import (
|
|
ButtonChallengeGenerator,
|
|
CaptchaChallengeGenerator,
|
|
ChallengeType,
|
|
EmojiChallengeGenerator,
|
|
MathChallengeGenerator,
|
|
VerificationService,
|
|
)
|
|
|
|
|
|
class TestChallengeGenerators:
|
|
"""Tests for challenge generators."""
|
|
|
|
def test_button_challenge(self) -> None:
|
|
"""Test button challenge generation."""
|
|
gen = ButtonChallengeGenerator()
|
|
challenge = gen.generate()
|
|
|
|
assert challenge.challenge_type == ChallengeType.BUTTON
|
|
assert challenge.answer == "verified"
|
|
|
|
def test_captcha_challenge(self) -> None:
|
|
"""Test captcha challenge generation."""
|
|
gen = CaptchaChallengeGenerator(length=6)
|
|
challenge = gen.generate()
|
|
|
|
assert challenge.challenge_type == ChallengeType.CAPTCHA
|
|
assert len(challenge.answer) == 6
|
|
assert challenge.answer.isalnum()
|
|
|
|
def test_math_challenge(self) -> None:
|
|
"""Test math challenge generation."""
|
|
gen = MathChallengeGenerator()
|
|
challenge = gen.generate()
|
|
|
|
assert challenge.challenge_type == ChallengeType.MATH
|
|
# Answer should be a number
|
|
assert challenge.answer.lstrip("-").isdigit()
|
|
|
|
def test_emoji_challenge(self) -> None:
|
|
"""Test emoji challenge generation."""
|
|
gen = EmojiChallengeGenerator()
|
|
challenge = gen.generate()
|
|
|
|
assert challenge.challenge_type == ChallengeType.EMOJI
|
|
assert len(challenge.options) > 0
|
|
assert challenge.answer in challenge.options
|
|
|
|
|
|
class TestVerificationService:
|
|
"""Tests for verification service."""
|
|
|
|
@pytest.fixture
|
|
def service(self) -> VerificationService:
|
|
return VerificationService()
|
|
|
|
def test_create_challenge(self, service: VerificationService) -> None:
|
|
"""Test creating a challenge."""
|
|
pending = service.create_challenge(
|
|
user_id=123,
|
|
guild_id=456,
|
|
challenge_type=ChallengeType.BUTTON,
|
|
)
|
|
|
|
assert pending.user_id == 123
|
|
assert pending.guild_id == 456
|
|
assert pending.challenge.challenge_type == ChallengeType.BUTTON
|
|
|
|
def test_get_pending(self, service: VerificationService) -> None:
|
|
"""Test retrieving pending verification."""
|
|
service.create_challenge(123, 456, ChallengeType.BUTTON)
|
|
|
|
pending = service.get_pending(456, 123)
|
|
assert pending is not None
|
|
assert pending.user_id == 123
|
|
|
|
# Non-existent should return None
|
|
assert service.get_pending(456, 999) is None
|
|
|
|
def test_verify_correct(self, service: VerificationService) -> None:
|
|
"""Test successful verification."""
|
|
service.create_challenge(123, 456, ChallengeType.BUTTON)
|
|
|
|
success, message = service.verify(456, 123, "verified")
|
|
assert success is True
|
|
assert "successful" in message.lower()
|
|
|
|
# Should be removed after success
|
|
assert service.get_pending(456, 123) is None
|
|
|
|
def test_verify_incorrect(self, service: VerificationService) -> None:
|
|
"""Test failed verification."""
|
|
service.create_challenge(123, 456, ChallengeType.BUTTON)
|
|
|
|
success, message = service.verify(456, 123, "wrong")
|
|
assert success is False
|
|
assert "incorrect" in message.lower()
|
|
|
|
# Should still exist
|
|
assert service.get_pending(456, 123) is not None
|
|
|
|
def test_verify_max_attempts(self, service: VerificationService) -> None:
|
|
"""Test max attempts exceeded."""
|
|
service.create_challenge(123, 456, ChallengeType.BUTTON)
|
|
|
|
# Use up all attempts
|
|
for _ in range(3):
|
|
service.verify(456, 123, "wrong")
|
|
|
|
success, message = service.verify(456, 123, "verified")
|
|
assert success is False
|
|
assert "too many" in message.lower()
|
|
|
|
def test_verify_no_pending(self, service: VerificationService) -> None:
|
|
"""Test verification with no pending challenge."""
|
|
success, message = service.verify(456, 123, "verified")
|
|
assert success is False
|
|
assert "no pending" in message.lower()
|
|
|
|
def test_cancel(self, service: VerificationService) -> None:
|
|
"""Test canceling verification."""
|
|
service.create_challenge(123, 456, ChallengeType.BUTTON)
|
|
|
|
assert service.cancel(456, 123) is True
|
|
assert service.get_pending(456, 123) is None
|
|
|
|
# Cancel non-existent returns False
|
|
assert service.cancel(456, 123) is False
|
|
|
|
def test_pending_count(self, service: VerificationService) -> None:
|
|
"""Test pending count per guild."""
|
|
service.create_challenge(1, 456, ChallengeType.BUTTON)
|
|
service.create_challenge(2, 456, ChallengeType.BUTTON)
|
|
service.create_challenge(3, 789, ChallengeType.BUTTON)
|
|
|
|
assert service.get_pending_count(456) == 2
|
|
assert service.get_pending_count(789) == 1
|
|
assert service.get_pending_count(999) == 0
|