import json from urllib.parse import parse_qs, urlparse import pytest from apps.users.models import UserSocialAccount from apps.users.services import google_oauth as google pytestmark = [pytest.mark.integration, pytest.mark.django_db] def _flow_from_redirect(response): return parse_qs(urlparse(response.url).query)["flow"][0] def test_google_oauth_start_callback_and_flow_lookup(client, settings, mocker): settings.GOOGLE_OAUTH_CLIENT_ID = "client-id" settings.GOOGLE_OAUTH_REDIRECT_URI = "https://backend.test/callback" settings.GOOGLE_OAUTH_FRONTEND_CALLBACK_URL = "https://frontend.test/google" start = client.get("/api/auth/oauth/google/start") assert start.status_code == 302 and "accounts.google.com" in start.url state = parse_qs(urlparse(start.url).query)["state"][0] profile = google.GoogleProfile("google-555", "new@example.com", True, "New", "Person", "") exchange = mocker.patch("apps.users.api.views.exchange_code_for_google_profile", return_value=profile) callback = client.get("/api/auth/oauth/google/callback", {"state": state, "code": "auth-code"}) assert callback.status_code == 302 exchange.assert_called_once_with("auth-code") flow = _flow_from_redirect(callback) response = client.get("/api/auth/oauth/google/flow", {"flow": flow}) assert response.status_code == 200 assert response.json()["resolution"] == "new_account" assert client.get("/api/auth/oauth/google/flow", {"flow": "unknown"}).status_code == 400 assert client.get("/api/auth/oauth/google/callback", {"state": state, "code": "replay"}).status_code == 302 cancelled = client.get("/api/auth/oauth/google/callback", {"error": "access_denied"}) assert google.get_google_flow(_flow_from_redirect(cancelled))["status"] == "error" def test_google_api_complete_claim_and_linked_login(client, settings, mocker, user_factory): settings.GOOGLE_OAUTH_FRONTEND_CALLBACK_URL = "https://frontend.test/google" profile = google.GoogleProfile("google-556", "person@example.com", True, "Google", "Person", "") flow = google.create_google_flow(google.build_pending_google_flow_payload(profile)) mocker.patch("apps.users.services.google_oauth.generate_and_send_otp") mocker.patch("apps.users.services.google_oauth.verify_otp_code") complete = client.post("/api/auth/oauth/google/complete", data=json.dumps({"flow": flow, "mobile": "09197770056", "username": "google_api"}), content_type="application/json") assert complete.status_code == 200, complete.content assert complete.json()["status"] == "claim_required" assert client.post("/api/auth/oauth/google/claim/send-otp", data=json.dumps({"flow": flow}), content_type="application/json").status_code == 200 verified = client.post("/api/auth/oauth/google/claim/verify", data=json.dumps({"flow": flow, "code": "12345"}), content_type="application/json") assert verified.status_code == 200, verified.content assert verified.json()["status"] == "authenticated" social = UserSocialAccount.objects.get(provider_user_id="google-556") assert social.user.username == "google_api" mocker.patch("apps.users.api.views.consume_google_state") mocker.patch("apps.users.api.views.exchange_code_for_google_profile", return_value=profile) callback = client.get("/api/auth/oauth/google/callback", {"state": "fresh", "code": "code"}) assert callback.status_code == 302 linked = google.get_google_flow(_flow_from_redirect(callback)) assert linked["status"] == "authenticated" and linked["access_token"]