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"], )