"""Replay signup, OTP login, and password reset in the isolated test database. SMS and the short-lived OTP store are mocked. Run after the fixture seed with --settings=config.settings.test and BUNDLE0_TEST_PASSWORD set. """ import json import os from unittest.mock import patch from django.conf import settings from django.test import Client, override_settings from apps.users.models import User assert os.environ.get("DJANGO_SETTINGS_MODULE") == "config.settings.test" assert settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3" password = os.environ["BUNDLE0_TEST_PASSWORD"] mobile = "09198000113" client = Client() class TemporaryOtpStore: def __init__(self): self.values = {} def setex(self, key, _seconds, value): self.values[key] = str(value).encode("utf-8") def get(self, key): return self.values.get(key) def delete(self, key): self.values.pop(key, None) store = TemporaryOtpStore() def post(path, payload): return client.post(path, data=json.dumps(payload), content_type="application/json") with ( override_settings(ALLOWED_HOSTS=["testserver", "localhost"]), patch("apps.users.services.auth._redis", return_value=store), patch("apps.users.services.auth.send_critical_sms.delay"), ): invalid = post("/api/auth/otp/send", {"mobile": "123", "mode": "register"}) assert invalid.status_code == 400 sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "register"}) assert sent.status_code == 200, sent.content code = store.get(f"auth_otp:register:{mobile}").decode("utf-8") wrong = "00000" if code != "00000" else "99999" rejected = post("/api/auth/otp/verify-register", {"mobile": mobile, "code": wrong}) assert rejected.status_code == 400 verified = post("/api/auth/otp/verify-register", {"mobile": mobile, "code": code}) assert verified.status_code == 200, verified.content registered = post( "/api/auth/register", {"username": "bundle0_signup", "mobile": mobile, "code": code, "password": password}, ) assert registered.status_code == 201, registered.content assert User.objects.get(username="bundle0_signup").is_mobile_verified duplicate = post("/api/auth/otp/send", {"mobile": mobile, "mode": "register"}) assert duplicate.status_code == 400 sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "login"}) assert sent.status_code == 200, sent.content code = store.get(f"auth_otp:login:{mobile}").decode("utf-8") login = post("/api/auth/login/otp", {"mobile": mobile, "code": code}) assert login.status_code == 200 and login.json()["access_token"] sent = post("/api/auth/otp/send", {"mobile": mobile, "mode": "reset_password"}) assert sent.status_code == 200, sent.content code = store.get(f"auth_otp:reset_password:{mobile}").decode("utf-8") new_password = password + "New" rejected = post( "/api/auth/reset-password", {"mobile": mobile, "code": wrong if wrong != code else "88888", "new_password": new_password}, ) assert rejected.status_code == 400 changed = post( "/api/auth/reset-password", {"mobile": mobile, "code": code, "new_password": new_password}, ) assert changed.status_code == 200, changed.content old_login = post("/api/auth/login", {"identifier": mobile, "password": password}) new_login = post("/api/auth/login", {"identifier": mobile, "password": new_password}) assert old_login.status_code == 401 and new_login.status_code == 200 print("Auth replay: invalid OTP request and code rejected; signup, OTP login, and password reset succeeded; old password rejected.")