Le rôle "admin" contournait entièrement l'isolation par projet (core/auth_guard.py) : il pouvait ouvrir/modifier/supprimer le jeu de n'importe quel autre compte en connaissant son slug, et la page d'accueil listait sans filtrage tous les projets de tous les comptes. - La propriété d'un projet se vérifie désormais sur le segment "propriétaire" du slug (id du compte), pour tous les rôles y compris admin — un slug "à plat" (sans compte associé) reste réservé à l'admin, comportement historique conservé pour ce cas précis. - routes/games/index.py ne liste plus que les projets du compte connecté. - Le dossier propriétaire d'un projet est maintenant l'id numérique du compte, plus jamais son email slugifié (visible en clair dans chaque URL auparavant) — script de migration fourni et déjà exécuté sur les données existantes. - Changer d'email ne renomme plus aucun dossier (n'en dépend plus). - Deux nouveaux tests de régression, fixtures corrigées en conséquence. - README réécrit pour refléter l'état actuel du produit (jeu 2D uniquement, plus de traces de l'ancien éditeur "document").
271 lines
12 KiB
Python
271 lines
12 KiB
Python
"""Page de profil (routes/auth/profile.py) : modifier nom/prénom, changer
|
|
de mot de passe, demander la suppression de son compte (avec mot de passe
|
|
+ confirmation textuelle, voir templates/auth/profile.html)."""
|
|
import pyotp
|
|
|
|
import auth
|
|
import db
|
|
|
|
from tests.test_auth import _register, _confirm_2fa, _complete_onboarding, _cleanup_project, anon_client # noqa: F401
|
|
|
|
|
|
def test_profile_page_shows_current_user_info(anon_client):
|
|
_register(anon_client, "profileinfo@example.com", prenom="Ada", nom="Lovelace")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
html = anon_client.get("/profile").get_data(as_text=True)
|
|
assert "profileinfo@example.com" in html
|
|
assert 'value="Ada"' in html
|
|
assert 'value="Lovelace"' in html
|
|
finally:
|
|
_cleanup_project("profileinfo@example.com")
|
|
|
|
|
|
def test_update_name(anon_client):
|
|
_register(anon_client, "updatename@example.com", prenom="Old", nom="Name")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/update-name", data={"prenom": "New", "nom": "Name2"})
|
|
assert "mises à jour" in resp.get_data(as_text=True)
|
|
user = auth.get_user_by_email("updatename@example.com")
|
|
assert user["prenom"] == "New"
|
|
assert user["nom"] == "Name2"
|
|
finally:
|
|
_cleanup_project("updatename@example.com")
|
|
|
|
|
|
def test_update_name_requires_both_fields(anon_client):
|
|
_register(anon_client, "updatenameblank@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/update-name", data={"prenom": "", "nom": "Test"})
|
|
assert "requis" in resp.get_data(as_text=True)
|
|
finally:
|
|
_cleanup_project("updatenameblank@example.com")
|
|
|
|
|
|
def test_change_password_requires_correct_current_password(anon_client):
|
|
_register(anon_client, "changepwwrong@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/change-password", data={
|
|
"current_password": "wrong", "new_password": "Nouveau$ecret1", "new_password_confirm": "Nouveau$ecret1",
|
|
})
|
|
assert "incorrect" in resp.get_data(as_text=True)
|
|
finally:
|
|
_cleanup_project("changepwwrong@example.com")
|
|
|
|
|
|
def test_change_password_rejects_mismatched_confirmation(anon_client):
|
|
_register(anon_client, "changepwmismatch@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/change-password", data={
|
|
"current_password": "Sup3r$ecret!", "new_password": "Nouveau$ecret1", "new_password_confirm": "Autre$ecret2",
|
|
})
|
|
assert "ne correspondent pas" in resp.get_data(as_text=True)
|
|
finally:
|
|
_cleanup_project("changepwmismatch@example.com")
|
|
|
|
|
|
def test_change_password_rejects_a_weak_new_password(anon_client):
|
|
_register(anon_client, "changepwweak@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/change-password", data={
|
|
"current_password": "Sup3r$ecret!", "new_password": "faible", "new_password_confirm": "faible",
|
|
})
|
|
assert "faible" in resp.get_data(as_text=True)
|
|
finally:
|
|
_cleanup_project("changepwweak@example.com")
|
|
|
|
|
|
def test_change_password_success_lets_login_with_new_password(anon_client):
|
|
_register(anon_client, "changepwok@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/change-password", data={
|
|
"current_password": "Sup3r$ecret!", "new_password": "Nouveau$ecret1", "new_password_confirm": "Nouveau$ecret1",
|
|
})
|
|
assert "modifié" in resp.get_data(as_text=True)
|
|
|
|
anon_client.post("/logout")
|
|
resp = anon_client.post("/login", data={"email": "changepwok@example.com", "password": "Nouveau$ecret1"}, follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
assert resp.headers["Location"] == "/login/2fa"
|
|
finally:
|
|
_cleanup_project("changepwok@example.com")
|
|
|
|
|
|
def test_delete_account_requires_correct_password(anon_client):
|
|
_register(anon_client, "deletewrongpw@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/delete", data={"current_password": "wrong", "confirm_text": "SUPPRIMER"})
|
|
assert "incorrect" in resp.get_data(as_text=True)
|
|
assert auth.get_user_by_email("deletewrongpw@example.com") is not None
|
|
finally:
|
|
_cleanup_project("deletewrongpw@example.com")
|
|
|
|
|
|
def test_delete_account_requires_the_confirmation_phrase(anon_client):
|
|
_register(anon_client, "deletenoconfirm@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/delete", data={"current_password": "Sup3r$ecret!", "confirm_text": "oups"})
|
|
assert "annulée" in resp.get_data(as_text=True)
|
|
assert auth.get_user_by_email("deletenoconfirm@example.com") is not None
|
|
finally:
|
|
_cleanup_project("deletenoconfirm@example.com")
|
|
|
|
|
|
def test_delete_account_removes_user_and_their_project(anon_client):
|
|
_register(anon_client, "deleteforreal@example.com")
|
|
_confirm_2fa(anon_client)
|
|
_complete_onboarding(anon_client)
|
|
user = auth.get_user_by_email("deleteforreal@example.com")
|
|
slug = user["project_slug"]
|
|
import os
|
|
assert os.path.isdir(db.game_dir(slug))
|
|
|
|
resp = anon_client.post("/profile/delete", data={"current_password": "Sup3r$ecret!", "confirm_text": "SUPPRIMER"}, follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
assert resp.headers["Location"] == "/login"
|
|
assert auth.get_user_by_email("deleteforreal@example.com") is None
|
|
assert not os.path.isdir(db.game_dir(slug))
|
|
|
|
|
|
def test_the_only_admin_account_cannot_be_deleted(client):
|
|
"""`client` (conftest.py) est déjà connecté en tant qu'admin unique de
|
|
la session de tests."""
|
|
resp = client.post("/profile/delete", data={"current_password": "Test1234!", "confirm_text": "SUPPRIMER"})
|
|
assert "administrateur" in resp.get_data(as_text=True)
|
|
import tests.conftest as conftest_module
|
|
assert auth.get_user_by_id(conftest_module._TEST_ADMIN_ID) is not None
|
|
|
|
|
|
def test_regenerate_recovery_codes_requires_correct_password(anon_client):
|
|
_register(anon_client, "regencodeswrong@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/regenerate-recovery-codes", data={"current_password": "wrong"})
|
|
assert "incorrect" in resp.get_data(as_text=True)
|
|
finally:
|
|
_cleanup_project("regencodeswrong@example.com")
|
|
|
|
|
|
def test_regenerate_recovery_codes_replaces_the_old_ones_and_shows_them_once(anon_client):
|
|
_register(anon_client, "regencodesok@example.com")
|
|
_confirm_2fa(anon_client)
|
|
user = auth.get_user_by_email("regencodesok@example.com")
|
|
try:
|
|
old_codes = auth.generate_recovery_codes(user["id"])
|
|
|
|
resp = anon_client.post("/profile/regenerate-recovery-codes", data={"current_password": "Sup3r$ecret!"})
|
|
html = resp.get_data(as_text=True)
|
|
assert "recoveryCodesModal" in html
|
|
|
|
# Jamais réaffichés sur un chargement suivant.
|
|
html_again = anon_client.get("/profile").get_data(as_text=True)
|
|
assert "recoveryCodesModal" not in html_again
|
|
|
|
# Les anciens codes ne fonctionnent plus, mais le nouveau lot,
|
|
# visible dans le modal ci-dessus, doit être réellement stocké en
|
|
# base — vérifié indirectement via verify_recovery_code : aucun des
|
|
# anciens codes ne doit plus matcher.
|
|
for old_code in old_codes:
|
|
assert not auth.verify_recovery_code(user["id"], old_code)
|
|
finally:
|
|
_cleanup_project("regencodesok@example.com")
|
|
|
|
|
|
def test_update_email_requires_correct_password(anon_client):
|
|
_register(anon_client, "emailwrongpw@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/update-email", data={"email": "new@example.com", "current_password": "wrong"})
|
|
assert "incorrect" in resp.get_data(as_text=True)
|
|
assert auth.get_user_by_email("emailwrongpw@example.com") is not None
|
|
finally:
|
|
_cleanup_project("emailwrongpw@example.com")
|
|
|
|
|
|
def test_update_email_rejects_invalid_format(anon_client):
|
|
_register(anon_client, "emailinvalid@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/update-email", data={"email": "not-an-email", "current_password": "Sup3r$ecret!"})
|
|
assert "invalide" in resp.get_data(as_text=True)
|
|
finally:
|
|
_cleanup_project("emailinvalid@example.com")
|
|
|
|
|
|
def test_update_email_rejects_an_email_already_used(anon_client):
|
|
_register(anon_client, "emailtaken1@example.com")
|
|
_confirm_2fa(anon_client)
|
|
anon_client.post("/logout")
|
|
_register(anon_client, "emailtaken2@example.com")
|
|
_confirm_2fa(anon_client)
|
|
try:
|
|
resp = anon_client.post("/profile/update-email", data={"email": "emailtaken1@example.com", "current_password": "Sup3r$ecret!"})
|
|
assert "existe déjà" in resp.get_data(as_text=True)
|
|
finally:
|
|
_cleanup_project("emailtaken1@example.com")
|
|
_cleanup_project("emailtaken2@example.com")
|
|
|
|
|
|
def test_update_email_never_touches_the_project_folder(anon_client):
|
|
"""Le dossier propriétaire d'un projet est nommé d'après l'ID du
|
|
compte (voir routes/games/games_new.py, opaque et stable — jamais
|
|
l'email, voir scripts/migrate_owner_folders_to_user_id.py pour la
|
|
migration qui a retiré l'ancien schéma slugify(email)) : changer
|
|
d'adresse email ne doit donc plus jamais renommer quoi que ce soit sur
|
|
disque, ni changer project_slug."""
|
|
import os
|
|
|
|
_register(anon_client, "oldmail@example.com")
|
|
_confirm_2fa(anon_client)
|
|
_complete_onboarding(anon_client)
|
|
old_user = auth.get_user_by_email("oldmail@example.com")
|
|
old_slug = old_user["project_slug"]
|
|
assert os.path.isdir(db.game_dir(old_slug))
|
|
try:
|
|
resp = anon_client.post("/profile/update-email", data={"email": "newmail@example.com", "current_password": "Sup3r$ecret!"})
|
|
assert "mise à jour" in resp.get_data(as_text=True)
|
|
|
|
assert auth.get_user_by_email("oldmail@example.com") is None
|
|
new_user = auth.get_user_by_email("newmail@example.com")
|
|
assert new_user is not None
|
|
# Ni le slug ni le dossier sur disque n'ont bougé.
|
|
assert new_user["project_slug"] == old_slug
|
|
assert os.path.isdir(db.game_dir(old_slug))
|
|
|
|
# La connexion se fait désormais avec la nouvelle adresse.
|
|
anon_client.post("/logout")
|
|
resp = anon_client.post("/login", data={"email": "newmail@example.com", "password": "Sup3r$ecret!"}, follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
assert resp.headers["Location"] == "/login/2fa"
|
|
finally:
|
|
_cleanup_project("oldmail@example.com")
|
|
_cleanup_project("newmail@example.com")
|
|
|
|
|
|
def test_update_email_for_an_admin_does_not_touch_any_project_folder(client, game):
|
|
"""`client`/`game` (conftest.py) : compte admin, pas de project_slug
|
|
dédié — changer son adresse ne doit renommer aucun dossier. L'admin de
|
|
test est PARTAGÉ par toute la session de tests (voir conftest.py) :
|
|
son adresse est restaurée après coup pour ne pas perturber les autres
|
|
tests qui s'appuient dessus."""
|
|
import tests.conftest as conftest_module
|
|
|
|
try:
|
|
resp = client.post("/profile/update-email", data={"email": "newadminmail@example.com", "current_password": "Test1234!"})
|
|
assert "mise à jour" in resp.get_data(as_text=True)
|
|
|
|
user = auth.get_user_by_id(conftest_module._TEST_ADMIN_ID)
|
|
assert user["email"] == "newadminmail@example.com"
|
|
assert user["project_slug"] is None
|
|
assert db.game_meta(game) is not None
|
|
finally:
|
|
auth.update_email(conftest_module._TEST_ADMIN_ID, "admin@pytest.local")
|