test: migrate backend suites to pytest with module coverage gate

Split backend tests into unit and integration suites, add isolated fixtures and coverage enforcement, and update CI and test documentation. Cover current authentication, content, notification, payment, media, and admin flows; fix behaviors revealed by the new tests. Validate both suites and the per-module gate against local PostgreSQL.
This commit is contained in:
2026-09-26 16:50:29 +03:30
parent 3ec28638d1
commit ff99130938
76 changed files with 3271 additions and 1357 deletions

View File

@@ -0,0 +1,62 @@
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"]