import pytest from apps.users.models import User, UserSocialAccount from apps.users.services import google_oauth as google pytestmark = [pytest.mark.integration, pytest.mark.django_db] @pytest.fixture def google_profile(): return google.GoogleProfile("provider-123", "google-person@example.com", True, "Google", "Person", "") def _start(profile, **overrides): payload = google.build_pending_google_flow_payload(profile) payload.update(overrides) return google.create_google_flow(payload) def test_new_google_account_requires_claim_and_links_after_otp(google_profile, mocker): send = mocker.patch("apps.users.services.google_oauth.generate_and_send_otp") verify = mocker.patch("apps.users.services.google_oauth.verify_otp_code") flow = _start(google_profile) pending = google.complete_google_signup(flow=flow, mobile="09197770011", username="google_new") assert pending["status"] == "claim_required" and pending["resolution"] == "new_account" assert not User.objects.filter(username="google_new").exists() send.assert_called_once_with("09197770011", "google_claim") assert google.send_google_claim_otp(flow)["message"] assert send.call_count == 2 authenticated = google.verify_google_claim(flow, "12345") assert authenticated["status"] == "authenticated" and authenticated["access_token"] verify.assert_called_once_with(mobile="09197770011", code="12345", mode="google_claim") user = User.objects.get(username="google_new") assert user.is_mobile_verified and not user.has_usable_password() assert UserSocialAccount.objects.filter(user=user, provider_user_id="provider-123").exists() assert google.get_google_flow(flow)["status"] == "authenticated" def test_existing_email_claim_links_matching_user(google_profile, user_factory, mocker): existing = user_factory(email=google_profile.email, is_mobile_verified=False) mocker.patch("apps.users.services.google_oauth.generate_and_send_otp") mocker.patch("apps.users.services.google_oauth.verify_otp_code") flow = _start(google_profile) pending = google.complete_google_signup(flow=flow, mobile=existing.mobile) assert pending["resolution"] == "existing_email_claim" authenticated = google.verify_google_claim(flow, "12345") assert authenticated["status"] == "authenticated" existing.refresh_from_db() assert existing.is_mobile_verified and existing.is_email_verified assert UserSocialAccount.objects.get(provider_user_id=google_profile.provider_user_id).user_id == existing.pk @pytest.mark.parametrize("values,expected_code", [ ({"mobile": "bad", "username": "new"}, "google_flow_error"), ({"mobile": "09197770022"}, "google_missing_username"), ({"mobile": "09197770022", "username": "taken"}, "google_username_taken"), ({"mobile": "09197770022", "username": "new", "student_id": "123"}, "google_invalid_student_id"), ({"mobile": "09197770022", "username": "new", "major": "missing"}, "google_invalid_major"), ({"mobile": "09197770022", "username": "new", "university": "missing"}, "google_invalid_university"), ]) def test_google_signup_rejects_invalid_profile_data(google_profile, user_factory, mocker, values, expected_code): if values.get("username") == "taken": user_factory(username="taken") send = mocker.patch("apps.users.services.google_oauth.generate_and_send_otp") flow = _start(google_profile) with pytest.raises(google.GoogleOAuthFlowError) as error: google.complete_google_signup(flow=flow, **values) assert error.value.code == expected_code send.assert_not_called() assert not UserSocialAccount.objects.exists() def test_google_mobile_conflict_is_rejected(google_profile, user_factory, mocker): conflict = user_factory(mobile="09197770033", email="other@example.com") send = mocker.patch("apps.users.services.google_oauth.generate_and_send_otp") flow = _start(google_profile) with pytest.raises(google.GoogleOAuthFlowError) as error: google.complete_google_signup(flow=flow, mobile=conflict.mobile, username="new_google") assert error.value.code == "google_mobile_belongs_to_other_email" send.assert_not_called() def test_google_claim_rejects_missing_target_and_existing_email(google_profile, user_factory, mocker): mocker.patch("apps.users.services.google_oauth.verify_otp_code") missing = google.create_google_flow({"status": "claim_required", "resolution": "existing_email_claim", "target_user_id": 999999, "mobile": "09197770044", "google_profile": google._profile_payload(google_profile)}) with pytest.raises(google.GoogleOAuthFlowError) as error: google.verify_google_claim(missing, "12345") assert error.value.code == "google_email_claim_failed" user_factory(email=google_profile.email) new_flow = google.create_google_flow({"status": "claim_required", "resolution": "new_account", "username": "fresh", "mobile": "09197770045", "google_profile": google._profile_payload(google_profile)}) with pytest.raises(google.GoogleOAuthFlowError) as error: google.verify_google_claim(new_flow, "12345") assert error.value.code == "google_email_taken"