test: migrate backend suites to pytest with module coverage gate
Split backend tests into unit and integration suites, add isolated fixtures and coverage enforcement, and update CI and test documentation. Cover current authentication, content, notification, payment, media, and admin flows; fix behaviors revealed by the new tests. Validate both suites and the per-module gate against local PostgreSQL.
This commit is contained in:
89
apps/users/tests/integration/test_auth_service_edges.py
Normal file
89
apps/users/tests/integration/test_auth_service_edges.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import pytest
|
||||
|
||||
from apps.users.models import Major, University, User
|
||||
from apps.users.services import auth
|
||||
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.django_db]
|
||||
|
||||
|
||||
def test_otp_send_modes_and_login_otp(user_factory, fake_redis, monkeypatch, mocker):
|
||||
monkeypatch.setattr(auth, "_redis", lambda: fake_redis)
|
||||
sms = mocker.patch("apps.users.services.auth.send_critical_sms.delay")
|
||||
user = user_factory(is_mobile_verified=False)
|
||||
with pytest.raises(auth.AuthServiceError):
|
||||
auth.generate_and_send_otp("bad", "register")
|
||||
with pytest.raises(auth.AuthServiceError):
|
||||
auth.generate_and_send_otp(user.mobile, "register")
|
||||
with pytest.raises(auth.AuthServiceError):
|
||||
auth.generate_and_send_otp("09197770061", "login")
|
||||
response = auth.generate_and_send_otp(user.mobile, "login")
|
||||
assert response["expires_in_seconds"] == auth.OTP_EXPIRY_SECONDS
|
||||
code = fake_redis.get(auth._otp_key("login", user.mobile)).decode()
|
||||
sms.assert_called_with(user.mobile, auth.SMS_KIND_LOGIN, code)
|
||||
tokens = auth.login_with_otp(user.mobile, code)
|
||||
assert tokens["access_token"] and tokens["refresh_token"]
|
||||
user.refresh_from_db()
|
||||
assert user.is_mobile_verified
|
||||
with pytest.raises(auth.AuthServiceError):
|
||||
auth.login_with_otp(user.mobile, code)
|
||||
|
||||
|
||||
def test_mobile_claim_conflict_and_verification(user_factory, fake_redis, monkeypatch, mocker):
|
||||
monkeypatch.setattr(auth, "_redis", lambda: fake_redis)
|
||||
sms = mocker.patch("apps.users.services.auth.send_critical_sms.delay")
|
||||
user = user_factory()
|
||||
another = user_factory()
|
||||
with pytest.raises(auth.AuthServiceError):
|
||||
auth.send_authenticated_mobile_otp(user, another.mobile)
|
||||
new_mobile = "09197770062"
|
||||
auth.send_authenticated_mobile_otp(user, new_mobile)
|
||||
code = fake_redis.get(auth._otp_key("verify_mobile", new_mobile)).decode()
|
||||
sms.assert_called_with(new_mobile, auth.SMS_KIND_VERIFY_MOBILE, code)
|
||||
assert auth.verify_authenticated_mobile_otp(user, new_mobile, code).mobile == new_mobile
|
||||
user.refresh_from_db()
|
||||
assert user.mobile == new_mobile and user.is_mobile_verified
|
||||
assert auth.lookup_mobile_registration_state(new_mobile) == {"exists": True, "has_password": True}
|
||||
assert auth.lookup_mobile_registration_state("09197770063") == {"exists": False, "has_password": False}
|
||||
|
||||
|
||||
def test_registration_validates_profile_fields_and_duplicate_email(user_factory, fake_redis, monkeypatch):
|
||||
monkeypatch.setattr(auth, "_redis", lambda: fake_redis)
|
||||
existing = user_factory(email="used@example.com")
|
||||
payload = auth.RegistrationPayload(username="new_profile", mobile="09197770064", password="StrongPass123!", code="12345")
|
||||
fake_redis.set(auth._verified_otp_key("register", payload.mobile), "1")
|
||||
for changes, field in [({"student_id": "123"}, "student_id"),
|
||||
({"email": existing.email}, "email"),
|
||||
({"major": "invalid"}, "major"),
|
||||
({"university": "invalid"}, "university")]:
|
||||
fake_redis.set(auth._verified_otp_key("register", payload.mobile), "1")
|
||||
current = auth.RegistrationPayload(**{**payload.__dict__, **changes})
|
||||
with pytest.raises(auth.AuthServiceError) as error:
|
||||
auth.register_user(current)
|
||||
assert error.value.field == field
|
||||
major = Major.objects.create(code="TEST_AUTH_MAJOR", name="Auth Major")
|
||||
university = University.objects.create(code="TEST_AUTH_UNI", name="Auth University")
|
||||
current = auth.RegistrationPayload(**{**payload.__dict__, "major": major.code, "university": university.code,
|
||||
"student_id": "1234567890"})
|
||||
fake_redis.set(auth._verified_otp_key("register", payload.mobile), "1")
|
||||
created = auth.register_user(current)
|
||||
assert created.major == major and created.university == university
|
||||
assert created.student_id == "1234567890"
|
||||
assert User.objects.filter(username=payload.username).count() == 1
|
||||
|
||||
|
||||
def test_password_reset_rejects_reuse_and_inactive_login(user_factory, fake_redis, monkeypatch):
|
||||
monkeypatch.setattr(auth, "_redis", lambda: fake_redis)
|
||||
user = user_factory(is_active=False)
|
||||
with pytest.raises(auth.AuthServiceError) as error:
|
||||
auth.login_with_password(user.mobile, "TestPass123!")
|
||||
assert error.value.status_code == 401
|
||||
user.is_active = True
|
||||
user.save(update_fields=["is_active"])
|
||||
with pytest.raises(auth.AuthServiceError):
|
||||
auth.reset_password_with_otp("09197770065", "12345", "NewStrongPass123!")
|
||||
fake_redis.set(auth._otp_key("reset_password", user.mobile), "12345")
|
||||
with pytest.raises(auth.AuthServiceError) as error:
|
||||
auth.reset_password_with_otp(user.mobile, "12345", "TestPass123!")
|
||||
assert error.value.field == "password"
|
||||
assert user.check_password("TestPass123!")
|
||||
Reference in New Issue
Block a user