Files
2026-09-22 00:50:30 +05:00

251 lines
11 KiB
Python

from datetime import date, timedelta
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APIClient
from locations.models import Atoll, Island
from users.models import OtpCode, RegistrationTicket, User
class RegistrationFlowTests(TestCase):
def setUp(self):
self.client = APIClient()
self.atoll = Atoll.objects.get(name="Faafu")
self.island = Island.objects.get(name="Dharanboodhoo")
self.other_atoll = Atoll.objects.create(name="Kaafu", code="K")
self.other_island = Island.objects.create(atoll=self.other_atoll, name="Male")
# --- verifying the number -------------------------------------------
def test_unknown_number_gets_a_code_without_being_told_anything(self):
response = self.client.post(reverse("auth-start"), {"mobile": "7700000"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["next"], "otp")
self.assertEqual(response.data["mobile"], "+9607700000")
otp = OtpCode.objects.get(mobile="+9607700000")
self.assertEqual(otp.purpose, OtpCode.Purpose.REGISTRATION)
self.assertIsNone(otp.user)
def test_verifying_an_unknown_number_hands_back_a_ticket(self):
code = self._send_registration_code("7700000")
response = self.client.post(
reverse("auth-verify"), {"mobile": "7700000", "code": code}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["next"], "register")
self.assertTrue(response.data["registration_token"])
self.assertEqual(response.data["mobile"], "+9607700000")
self.assertNotIn("token", response.data)
def test_verify_rejects_a_wrong_code(self):
self._send_registration_code("7700000")
response = self.client.post(
reverse("auth-verify"), {"mobile": "7700000", "code": "000000"}
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data["code"], "invalid_code")
self.assertFalse(RegistrationTicket.objects.exists())
def test_a_code_signs_in_an_account_created_in_the_meantime(self):
code = self._send_registration_code("7700000")
User.objects.create_user(mobile="7700000")
response = self.client.post(
reverse("auth-verify"), {"mobile": "7700000", "code": code}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["next"], "dashboard")
self.assertFalse(RegistrationTicket.objects.exists())
# --- the form -------------------------------------------------------
def test_registration_creates_a_pending_account(self):
token = self._verified_token("7700000")
response = self.client.post(reverse("auth-register"), self._form(token))
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data["status"], "pending")
# No token: registering does not sign anyone in.
self.assertNotIn("token", response.data)
user = User.objects.get(mobile="+9607700000")
self.assertEqual(user.status, User.Status.PENDING)
self.assertEqual(user.full_name, "Ahmed Ali")
self.assertEqual(user.idnumber, "WP123456")
self.assertEqual(user.date_of_birth, date(1995, 4, 17))
self.assertEqual(user.island, self.island)
self.assertTrue(user.mobile_verified)
self.assertFalse(user.has_usable_password())
self.assertIsNotNone(user.terms_accepted_at)
self.assertIsNotNone(user.policy_accepted_at)
def test_the_number_comes_from_the_token_not_the_form(self):
token = self._verified_token("7700000")
payload = self._form(token) | {"mobile": "7711111"}
self.client.post(reverse("auth-register"), payload)
self.assertTrue(User.objects.filter(mobile="+9607700000").exists())
self.assertFalse(User.objects.filter(mobile="+9607711111").exists())
def test_a_token_works_only_once(self):
token = self._verified_token("7700000")
self.client.post(reverse("auth-register"), self._form(token))
User.objects.all().delete()
response = self.client.post(reverse("auth-register"), self._form(token))
self.assertEqual(response.status_code, 400)
self.assertIn("registration_token", response.data["errors"])
def test_an_expired_token_is_refused(self):
token = self._verified_token("7700000")
RegistrationTicket.objects.filter(key=token).update(
expires_at=timezone.now() - timedelta(minutes=1)
)
response = self.client.post(reverse("auth-register"), self._form(token))
self.assertEqual(response.status_code, 400)
self.assertIn("registration_token", response.data["errors"])
def test_registration_requires_a_token(self):
payload = self._form("nope")
response = self.client.post(reverse("auth-register"), payload)
self.assertEqual(response.status_code, 400)
self.assertIn("registration_token", response.data["errors"])
def test_both_agreements_are_required(self):
for field in ["terms_accepted", "policy_accepted"]:
with self.subTest(field=field):
token = self._verified_token("7700000")
payload = self._form(token) | {field: False}
response = self.client.post(reverse("auth-register"), payload)
self.assertEqual(response.status_code, 400)
self.assertIn(field, response.data["errors"])
self.assertFalse(User.objects.exists())
def test_island_must_belong_to_the_selected_atoll(self):
token = self._verified_token("7700000")
payload = self._form(token) | {"island": self.other_island.pk}
response = self.client.post(reverse("auth-register"), payload)
self.assertEqual(response.status_code, 400)
self.assertIn("island", response.data["errors"])
def test_required_fields(self):
token = self._verified_token("7700000")
response = self.client.post(
reverse("auth-register"), {"registration_token": token}
)
self.assertEqual(response.status_code, 400)
for field in [
"full_name",
"idnumber",
"date_of_birth",
"atoll",
"island",
]:
self.assertIn(field, response.data["errors"])
def test_future_date_of_birth_is_refused(self):
token = self._verified_token("7700000")
payload = self._form(token) | {
"date_of_birth": (date.today() + timedelta(days=1)).isoformat()
}
response = self.client.post(reverse("auth-register"), payload)
self.assertEqual(response.status_code, 400)
self.assertIn("date_of_birth", response.data["errors"])
def test_cannot_register_a_number_that_already_has_an_account(self):
token = self._verified_token("7700000")
User.objects.create_user(mobile="7700000")
response = self.client.post(reverse("auth-register"), self._form(token))
self.assertEqual(response.status_code, 400)
self.assertIn("mobile", response.data["errors"])
# --- after submitting ------------------------------------------------
def test_a_pending_applicant_can_sign_in_and_see_their_status(self):
token = self._verified_token("7700000")
self.client.post(reverse("auth-register"), self._form(token))
start = self.client.post(reverse("auth-start"), {"mobile": "7700000"})
self.assertEqual(start.data["next"], "otp")
otp = OtpCode.objects.active().filter(mobile="+9607700000").first()
self.assertIsNotNone(otp)
self.assertEqual(otp.purpose, OtpCode.Purpose.LOGIN)
_, code = OtpCode.issue(
mobile="+9607700000",
purpose=OtpCode.Purpose.LOGIN,
user=User.objects.get(mobile="+9607700000"),
)
response = self.client.post(
reverse("auth-verify"), {"mobile": "7700000", "code": code}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["next"], "dashboard")
self.assertEqual(response.data["user"]["status"], "pending")
def test_approval_flips_the_status_and_records_the_reviewer(self):
admin = User.objects.create_superuser(mobile="7711111", password="x")
token = self._verified_token("7700000")
self.client.post(reverse("auth-register"), self._form(token))
user = User.objects.get(mobile="+9607700000")
user.approve(reviewer=admin)
user.refresh_from_db()
self.assertEqual(user.status, User.Status.APPROVED)
self.assertTrue(user.is_approved)
self.assertEqual(user.reviewed_by, admin)
self.assertIsNotNone(user.reviewed_at)
def test_rejection_records_the_reason(self):
token = self._verified_token("7700000")
self.client.post(reverse("auth-register"), self._form(token))
user = User.objects.get(mobile="+9607700000")
user.reject(reason="Document unreadable")
user.refresh_from_db()
self.assertEqual(user.status, User.Status.REJECTED)
self.assertEqual(user.rejection_reason, "Document unreadable")
# --- helpers ---------------------------------------------------------
def _send_registration_code(self, mobile: str) -> str:
otp, code = OtpCode.issue(
mobile=f"+960{mobile}", purpose=OtpCode.Purpose.REGISTRATION
)
self.assertIsNotNone(otp)
return code
def _verified_token(self, mobile: str) -> str:
code = self._send_registration_code(mobile)
response = self.client.post(
reverse("auth-verify"), {"mobile": mobile, "code": code}
)
self.assertEqual(response.status_code, 200, response.data)
return response.data["registration_token"]
def _form(self, token: str) -> dict:
return {
"registration_token": token,
"full_name": "Ahmed Ali",
"idnumber": "wp123456",
"date_of_birth": "1995-04-17",
"atoll": self.atoll.pk,
"island": self.island.pk,
"terms_accepted": True,
"policy_accepted": True,
}
class LocationsApiTests(TestCase):
def test_atolls_are_public_and_include_islands(self):
response = APIClient().get(reverse("atoll-list"))
self.assertEqual(response.status_code, 200)
names = {atoll["name"]: atoll for atoll in response.data}
self.assertIn("Faafu", names)
self.assertEqual(
[island["name"] for island in names["Faafu"]["islands"]],
["Dharanboodhoo"],
)