Spaces:
Runtime error
Runtime error
Claude Opus 5 (1M context)
feat(auth): inscription publique, connexion Google, et conversations rattachees a un compte
d21ea1e Download tests/test_api/test_google_auth_routes.py from rahim237cm/jurix-api: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/spaces/rahim237cm/jurix-api/resolve/main/tests/test_api/test_google_auth_routes.py
- Command line
-
hf download hf://spaces/rahim237cm/jurix-api/tests/test_api/test_google_auth_routes.py
-
curl -L -o test_google_auth_routes.py https://huggingface.co/spaces/rahim237cm/jurix-api/resolve/main/tests/test_api/test_google_auth_routes.py
11.8 kB
| """ | |
| Tests de POST /api/v1/auth/google. | |
| AUCUN APPEL RESEAU : `verifier_jeton_google` est double par monkeypatch. Le | |
| vrai module fait un GET HTTPS vers Google pour ses certificats — en dependre | |
| rendrait la suite lente et instable, et la ferait echouer hors ligne. | |
| Le test le plus important est `test_client_id_vide_repond_503_sans_verifier` : | |
| `verify_oauth2_token(..., audience=None)` DESACTIVE la verification de l'`aud`, | |
| ce qui ferait accepter n'importe quel jeton Google emis pour n'importe quelle | |
| application. La route doit donc refuser AVANT d'appeler la verification quand | |
| l'identifiant client manque. | |
| Usage: | |
| pytest tests/test_api/test_google_auth_routes.py -v | |
| """ | |
| import pytest | |
| from google.auth import exceptions as google_exceptions | |
| from httpx import AsyncClient | |
| from sqlalchemy import func, select | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| import app.api.routes.auth as routes_auth | |
| from app.core.config import settings | |
| from app.models.user import User | |
| CHEMIN = "/api/v1/auth/google" | |
| JETON = "j" * 200 # `credential` est borne a 100 caracteres minimum | |
| def claims(**surcharges): | |
| base = { | |
| "sub": "108234567890123456789", | |
| "email": "rahim@gmail.com", | |
| "email_verified": True, | |
| "name": "Rahim Sahane", | |
| } | |
| base.update(surcharges) | |
| return base | |
| def google_configure(monkeypatch): | |
| """Un identifiant client present, sinon la route refuse en 503.""" | |
| monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", "id-client-de-test.apps.googleusercontent.com") | |
| def google_ok(monkeypatch, google_configure): | |
| """Double la verification par un succes, et compte les appels.""" | |
| appels = [] | |
| async def _double(credential): | |
| appels.append(credential) | |
| return claims() | |
| monkeypatch.setattr(routes_auth, "verifier_jeton_google", _double) | |
| return appels | |
| def double_avec(monkeypatch, resultat=None, erreur=None): | |
| appels = [] | |
| async def _double(credential): | |
| appels.append(credential) | |
| if erreur is not None: | |
| raise erreur | |
| return resultat | |
| monkeypatch.setattr(routes_auth, "verifier_jeton_google", _double) | |
| return appels | |
| class TestCreationEtLiaison: | |
| async def test_premiere_connexion_cree_le_compte( | |
| self, client: AsyncClient, db_session: AsyncSession, google_ok | |
| ): | |
| r = await client.post(CHEMIN, json={"credential": JETON}) | |
| assert r.status_code == 200 | |
| donnees = r.json() | |
| assert donnees["email"] == "rahim@gmail.com" | |
| assert donnees["full_name"] == "Rahim Sahane" | |
| assert donnees["role"] == "user" | |
| assert donnees["is_verified"] is True, "Google a affirme email_verified" | |
| user = ( | |
| await db_session.execute(select(User).where(User.email == "rahim@gmail.com")) | |
| ).scalar_one() | |
| assert user.google_sub == claims()["sub"] | |
| assert user.hashed_password is None, "un compte Google n'a pas de mot de passe" | |
| async def test_seconde_connexion_ne_duplique_pas( | |
| self, client: AsyncClient, db_session: AsyncSession, google_ok | |
| ): | |
| await client.post(CHEMIN, json={"credential": JETON}) | |
| await client.post(CHEMIN, json={"credential": JETON}) | |
| combien = ( | |
| await db_session.execute( | |
| select(func.count()).select_from(User).where(User.email == "rahim@gmail.com") | |
| ) | |
| ).scalar() | |
| assert combien == 1 | |
| async def test_liaison_conserve_le_mot_de_passe( | |
| self, client: AsyncClient, db_session: AsyncSession, google_ok | |
| ): | |
| """ | |
| Un compte cree par mot de passe qui se connecte ensuite par Google gagne | |
| une porte, il n'en perd pas. Google prouve le controle de la boite — | |
| preuve plus forte que notre inscription, qui n'envoie aucun e-mail. | |
| """ | |
| inscription = await client.post( | |
| "/api/v1/auth/signup", | |
| json={ | |
| "full_name": "Rahim", | |
| "email": "rahim@gmail.com", | |
| "password": "MotDePasse1", | |
| }, | |
| ) | |
| assert inscription.status_code == 201 | |
| r = await client.post(CHEMIN, json={"credential": JETON}) | |
| assert r.status_code == 200 | |
| combien = ( | |
| await db_session.execute( | |
| select(func.count()).select_from(User).where(User.email == "rahim@gmail.com") | |
| ) | |
| ).scalar() | |
| assert combien == 1, "un second compte a ete cree au lieu d'une liaison" | |
| db_session.expire_all() | |
| user = ( | |
| await db_session.execute(select(User).where(User.email == "rahim@gmail.com")) | |
| ).scalar_one() | |
| assert user.google_sub is not None | |
| assert user.hashed_password is not None, "le mot de passe a ete perdu" | |
| # Et il fonctionne toujours. | |
| connexion = await client.post( | |
| "/api/v1/auth/login/json", | |
| json={"email": "rahim@gmail.com", "password": "MotDePasse1"}, | |
| ) | |
| assert connexion.status_code == 200 | |
| async def test_inscription_sur_un_compte_google_refusee( | |
| self, client: AsyncClient, db_session: AsyncSession, google_ok | |
| ): | |
| """ | |
| L'ASYMETRIE. L'inverse serait une prise de controle : sans verification | |
| d'adresse de notre cote, quiconque connait l'adresse Gmail de quelqu'un | |
| pourrait se greffer un mot de passe sur son compte. | |
| """ | |
| await client.post(CHEMIN, json={"credential": JETON}) | |
| r = await client.post( | |
| "/api/v1/auth/signup", | |
| json={ | |
| "full_name": "Usurpateur", | |
| "email": "rahim@gmail.com", | |
| "password": "MotDePasse1", | |
| }, | |
| ) | |
| assert r.status_code == 409 | |
| assert "Google" not in r.json()["detail"], "le message revele le fournisseur" | |
| db_session.expire_all() | |
| user = ( | |
| await db_session.execute(select(User).where(User.email == "rahim@gmail.com")) | |
| ).scalar_one() | |
| assert user.hashed_password is None, "un mot de passe a ete greffe" | |
| async def test_adresse_normalisee(self, client: AsyncClient, monkeypatch, google_configure): | |
| double_avec(monkeypatch, resultat=claims(email=" Rahim@GMAIL.com ")) | |
| r = await client.post(CHEMIN, json={"credential": JETON}) | |
| assert r.json()["email"] == "rahim@gmail.com" | |
| class TestRefus: | |
| async def test_client_id_vide_repond_503_sans_verifier( | |
| self, client: AsyncClient, monkeypatch, db_session: AsyncSession | |
| ): | |
| """ | |
| LE TEST LE PLUS IMPORTANT DU FICHIER. | |
| `verify_oauth2_token(..., audience=None)` DESACTIVE la verification de | |
| l'`aud` : n'importe quel jeton Google, emis pour n'importe quelle | |
| application tierce, serait alors accepte — contournement complet de | |
| l'authentification. La garde doit donc refuser AVANT d'appeler la | |
| bibliotheque. | |
| On espionne ici `verify_oauth2_token` LUI-MEME, et non | |
| `verifier_jeton_google` : doubler ce dernier remplacerait justement la | |
| garde qu'on veut eprouver, et le test passerait sans rien prouver. | |
| """ | |
| from app.core import google_identity | |
| monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", "") | |
| appels = [] | |
| def _espion(*a, **k): | |
| appels.append(a) | |
| return claims() | |
| monkeypatch.setattr(google_identity.id_token, "verify_oauth2_token", _espion) | |
| r = await client.post(CHEMIN, json={"credential": JETON}) | |
| assert r.status_code == 503 | |
| assert appels == [], "la verification a ete appelee sans identifiant client" | |
| combien = (await db_session.execute(select(func.count()).select_from(User))).scalar() | |
| assert combien == 0, "un compte a ete cree sans verification" | |
| async def test_jeton_invalide_401(self, client: AsyncClient, monkeypatch, google_configure): | |
| from app.core.google_identity import JetonGoogleInvalide | |
| double_avec(monkeypatch, erreur=JetonGoogleInvalide("adresse non verifiee")) | |
| r = await client.post(CHEMIN, json={"credential": JETON}) | |
| assert r.status_code == 401 | |
| # Message unique : detailler la cause aiderait qui cherche a fabriquer | |
| # un jeton acceptable. | |
| assert "verifiee" not in r.json()["detail"] | |
| async def test_google_injoignable_503(self, client: AsyncClient, monkeypatch, google_configure): | |
| """ | |
| NOTRE reseau a echoue, pas celui de l'utilisateur. Un 401 l'accuserait | |
| d'un defaut qui n'est pas le sien. | |
| """ | |
| from app.core.google_identity import GoogleInjoignable | |
| double_avec(monkeypatch, erreur=GoogleInjoignable("certificats injoignables")) | |
| r = await client.post(CHEMIN, json={"credential": JETON}) | |
| assert r.status_code == 503 | |
| assert r.headers.get("Retry-After") == "30" | |
| async def test_compte_desactive_403( | |
| self, client: AsyncClient, db_session: AsyncSession, google_ok | |
| ): | |
| await client.post(CHEMIN, json={"credential": JETON}) | |
| user = ( | |
| await db_session.execute(select(User).where(User.email == "rahim@gmail.com")) | |
| ).scalar_one() | |
| user.is_active = False | |
| await db_session.commit() | |
| r = await client.post(CHEMIN, json={"credential": JETON}) | |
| assert r.status_code == 403 | |
| async def test_422(self, client: AsyncClient, google_configure, corps): | |
| assert (await client.post(CHEMIN, json=corps)).status_code == 422 | |
| class TestVerificationReelle: | |
| """La fonction de verification elle-meme, sans passer par la route.""" | |
| async def test_email_non_verifie_refuse(self, monkeypatch): | |
| """ | |
| `verify_oauth2_token` ne verifie PAS `email_verified`. Sans ce controle, | |
| un compte Google dont l'adresse n'a jamais ete prouvee pourrait se lier | |
| a un compte JuriX existant portant la meme adresse. | |
| """ | |
| from app.core import google_identity | |
| monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", "id-de-test") | |
| monkeypatch.setattr( | |
| google_identity.id_token, | |
| "verify_oauth2_token", | |
| lambda *a, **k: claims(email_verified=False), | |
| ) | |
| with pytest.raises(google_identity.JetonGoogleInvalide): | |
| await google_identity.verifier_jeton_google(JETON) | |
| async def test_erreur_de_transport_distincte(self, monkeypatch): | |
| from app.core import google_identity | |
| monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", "id-de-test") | |
| def _lever(*a, **k): | |
| raise google_exceptions.TransportError("reseau") | |
| monkeypatch.setattr(google_identity.id_token, "verify_oauth2_token", _lever) | |
| with pytest.raises(google_identity.GoogleInjoignable): | |
| await google_identity.verifier_jeton_google(JETON) | |
| async def test_jeton_illisible(self, monkeypatch): | |
| from app.core import google_identity | |
| monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", "id-de-test") | |
| def _lever(*a, **k): | |
| raise ValueError("Token expired") | |
| monkeypatch.setattr(google_identity.id_token, "verify_oauth2_token", _lever) | |
| with pytest.raises(google_identity.JetonGoogleInvalide): | |
| await google_identity.verifier_jeton_google(JETON) | |