import pytest from django.contrib.auth.hashers import make_password from django.test import override_settings from apps.accounts.models import OtpChannel, OtpPurpose, PhoneOTP from apps.accounts.services.otp import OtpCooldownError, OtpRateLimitError, create_and_send_otp, verify_otp @pytest.mark.django_db @override_settings(OTP_PROVIDER="console", OTP_MAX_PER_WINDOW=1, OTP_WINDOW_MINUTES=15, OTP_RESEND_COOLDOWN_SECONDS=0) def test_otp_rate_limit(): create_and_send_otp("+966512345678", OtpChannel.SMS) with pytest.raises(OtpRateLimitError): create_and_send_otp("+966512345678", OtpChannel.SMS) @pytest.mark.django_db @override_settings( OTP_PROVIDER="console", OTP_MAX_PER_WINDOW=5, OTP_WINDOW_MINUTES=15, OTP_RESEND_COOLDOWN_SECONDS=60, ) def test_otp_cooldown_enforced(): create_and_send_otp("+966512345678", OtpChannel.SMS) with pytest.raises(OtpCooldownError): create_and_send_otp("+966512345678", OtpChannel.SMS) @pytest.mark.django_db def test_otp_max_attempts_blocks_verification(): otp = PhoneOTP.objects.create( phone_number="+966512345678", channel=OtpChannel.SMS, purpose=OtpPurpose.AUTH, provider="console", code_hash=make_password("123456"), expires_at=PhoneOTP.expiry_at(), ) # Burn attempts with wrong code until the limit is exceeded. for _ in range(otp.max_attempts): assert verify_otp(otp, "000000") is False otp.refresh_from_db() assert otp.attempt_count == otp.max_attempts assert verify_otp(otp, "123456") is False otp.refresh_from_db() assert otp.attempt_count == otp.max_attempts + 1 assert otp.verified_at is None