Ajoute une page de profil (modifier ses infos, mot de passe, supprimer son compte)
Le nom affiché dans la barre de navigation (base.html) devient un lien vers /profile : informations du compte (email en lecture seule, nom/ prénom modifiables), changement de mot de passe (mot de passe actuel requis + mêmes règles de force qu'à l'inscription/la réinitialisation), et une section "danger" pour demander la suppression du compte. La suppression exige le mot de passe actuel ET la saisie exacte de "SUPPRIMER" (deux confirmations distinctes pour une action irréversible) — routes/auth/profile.py. Pour un compte "user" (limité à un seul projet, créé automatiquement et impossible à supprimer autrement, voir core/auth_guard.py), supprimer le compte supprime aussi son unique projet sur le disque, faute de quoi il resterait orphelin sans plus aucun propriétaire. Un compte "admin" peut posséder plusieurs jeux qui ne lui sont pas dédiés de la même façon : ses projets ne sont jamais touchés. L'unique compte administrateur ne peut pas être supprimé (auth/ count_admins.py) — le supprimer bloquerait la création d'un nouveau compte admin (réservée au tout premier compte jamais créé, base vide). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d5a84413d1
commit
0d46abe193
@@ -26,6 +26,9 @@ from .password_reset import (
|
|||||||
TOKEN_TTL_MINUTES,
|
TOKEN_TTL_MINUTES,
|
||||||
)
|
)
|
||||||
from .send_email import send_password_reset_email, EmailNotConfiguredError
|
from .send_email import send_password_reset_email, EmailNotConfiguredError
|
||||||
|
from .update_profile import update_profile
|
||||||
|
from .count_admins import count_admins
|
||||||
|
from .delete_user import delete_user
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"users_db_path", "ensure_users_schema", "is_first_user",
|
"users_db_path", "ensure_users_schema", "is_first_user",
|
||||||
@@ -40,4 +43,5 @@ __all__ = [
|
|||||||
"set_password", "create_password_reset_token", "get_user_id_for_valid_token",
|
"set_password", "create_password_reset_token", "get_user_id_for_valid_token",
|
||||||
"consume_password_reset_token", "TOKEN_TTL_MINUTES",
|
"consume_password_reset_token", "TOKEN_TTL_MINUTES",
|
||||||
"send_password_reset_email", "EmailNotConfiguredError",
|
"send_password_reset_email", "EmailNotConfiguredError",
|
||||||
|
"update_profile", "count_admins", "delete_user",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from .connection import connect
|
||||||
|
|
||||||
|
|
||||||
|
def count_admins():
|
||||||
|
conn = connect()
|
||||||
|
n = conn.execute("SELECT COUNT(*) AS n FROM _users WHERE role = 'admin'").fetchone()["n"]
|
||||||
|
conn.close()
|
||||||
|
return n
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from .connection import connect
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user(user_id):
|
||||||
|
"""Supprime le compte et tout ce qui lui est directement rattaché
|
||||||
|
(codes de récupération, jetons de réinitialisation encore en cours) —
|
||||||
|
le dossier de projet, lui, est géré par l'appelant (voir
|
||||||
|
routes/auth/profile.py) : un compte "admin" peut posséder plusieurs
|
||||||
|
jeux qu'il n'a pas forcément envie de perdre en même temps que son
|
||||||
|
compte."""
|
||||||
|
conn = connect()
|
||||||
|
conn.execute("DELETE FROM _recovery_codes WHERE user_id = ?", (user_id,))
|
||||||
|
conn.execute("DELETE FROM _password_reset_tokens WHERE user_id = ?", (user_id,))
|
||||||
|
conn.execute("DELETE FROM _users WHERE id = ?", (user_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from .connection import connect
|
||||||
|
|
||||||
|
|
||||||
|
def update_profile(user_id, nom, prenom):
|
||||||
|
conn = connect()
|
||||||
|
conn.execute("UPDATE _users SET nom = ?, prenom = ? WHERE id = ?", (nom, prenom, user_id))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
@@ -1 +1 @@
|
|||||||
from . import register, register_2fa, login, login_2fa, logout, forgot_password, reset_password
|
from . import register, register_2fa, login, login_2fa, logout, forgot_password, reset_password, profile
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from flask import g, redirect, render_template, request, session, url_for
|
||||||
|
|
||||||
|
import auth
|
||||||
|
import db
|
||||||
|
|
||||||
|
from core.flask_app import app
|
||||||
|
|
||||||
|
|
||||||
|
def _render(error=None, success=None):
|
||||||
|
user = auth.get_user_by_id(g.current_user["id"])
|
||||||
|
return render_template(
|
||||||
|
"auth/profile.html", user=user, error=error, success=success,
|
||||||
|
min_score_required=auth.MIN_SCORE_REQUIRED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/profile", methods=["GET"])
|
||||||
|
def profile():
|
||||||
|
return _render()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/profile/update-name", methods=["POST"])
|
||||||
|
def profile_update_name():
|
||||||
|
nom = request.form.get("nom", "").strip()
|
||||||
|
prenom = request.form.get("prenom", "").strip()
|
||||||
|
if not nom or not prenom:
|
||||||
|
return _render(error="Nom et prénom sont requis.")
|
||||||
|
auth.update_profile(g.current_user["id"], nom, prenom)
|
||||||
|
return _render(success="Informations mises à jour.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/profile/change-password", methods=["POST"])
|
||||||
|
def profile_change_password():
|
||||||
|
user = g.current_user
|
||||||
|
current_password = request.form.get("current_password", "")
|
||||||
|
new_password = request.form.get("new_password", "")
|
||||||
|
new_password_confirm = request.form.get("new_password_confirm", "")
|
||||||
|
if not auth.verify_password(user, current_password):
|
||||||
|
return _render(error="Mot de passe actuel incorrect.")
|
||||||
|
if new_password != new_password_confirm:
|
||||||
|
return _render(error="Les nouveaux mots de passe ne correspondent pas.")
|
||||||
|
if not auth.password_strength(new_password)["valid"]:
|
||||||
|
return _render(error="Nouveau mot de passe trop faible — respecte les critères affichés.")
|
||||||
|
auth.set_password(user["id"], new_password)
|
||||||
|
return _render(success="Mot de passe modifié.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/profile/delete", methods=["POST"])
|
||||||
|
def profile_delete():
|
||||||
|
user = g.current_user
|
||||||
|
current_password = request.form.get("current_password", "")
|
||||||
|
confirm_text = request.form.get("confirm_text", "").strip().upper()
|
||||||
|
if not auth.verify_password(user, current_password):
|
||||||
|
return _render(error="Mot de passe incorrect — suppression annulée.")
|
||||||
|
if confirm_text != "SUPPRIMER":
|
||||||
|
return _render(error='Tape "SUPPRIMER" pour confirmer — suppression annulée.')
|
||||||
|
if user["role"] == "admin" and auth.count_admins() <= 1:
|
||||||
|
return _render(error="Impossible de supprimer l'unique compte administrateur.")
|
||||||
|
|
||||||
|
# Un compte "user" ne possède qu'UN SEUL projet, créé automatiquement
|
||||||
|
# pour lui et impossible à supprimer autrement (voir core/auth_guard.py,
|
||||||
|
# game_delete bloqué pour ce rôle) — supprimer le compte est donc la
|
||||||
|
# seule façon de s'en débarrasser aussi, sans quoi il resterait orphelin
|
||||||
|
# sur le disque sans plus aucun propriétaire. Un "admin" peut posséder
|
||||||
|
# plusieurs jeux qui ne lui sont pas dédiés de la même façon : ses
|
||||||
|
# projets restent intacts.
|
||||||
|
if user["role"] != "admin" and user.get("project_slug"):
|
||||||
|
game_dir = db.game_dir(user["project_slug"])
|
||||||
|
if os.path.isdir(game_dir):
|
||||||
|
shutil.rmtree(game_dir)
|
||||||
|
|
||||||
|
auth.delete_user(user["id"])
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for("login"))
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Mon profil — Forge Engine{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container" style="max-width:560px;">
|
||||||
|
<h1 class="title">Mon profil</h1>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="notification is-danger">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if success %}
|
||||||
|
<div class="notification is-success">{{ success }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h2 class="title is-5">Informations</h2>
|
||||||
|
<p class="hint mb-4">Adresse email : <strong>{{ user.email }}</strong> ({{ 'compte administrateur' if user.role == 'admin' else 'compte utilisateur' }})</p>
|
||||||
|
<form method="post" action="{{ url_for('profile_update_name') }}">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Prénom</label>
|
||||||
|
<div class="control"><input class="input" type="text" name="prenom" value="{{ user.prenom }}" required></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Nom</label>
|
||||||
|
<div class="control"><input class="input" type="text" name="nom" value="{{ user.nom }}" required></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-4">
|
||||||
|
<button type="submit" class="button primary">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h2 class="title is-5">Changer de mot de passe</h2>
|
||||||
|
<form method="post" action="{{ url_for('profile_change_password') }}" autocomplete="off">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Mot de passe actuel</label>
|
||||||
|
<div class="control"><input class="input" type="password" name="current_password" required autocomplete="current-password"></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Nouveau mot de passe</label>
|
||||||
|
<div class="control"><input class="input" type="password" id="password" name="new_password" required autocomplete="new-password"></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="passwordStrengthBar"><div class="passwordStrengthFill" id="pwFill"></div></div>
|
||||||
|
<ul class="passwordChecklist" id="pwChecklist">
|
||||||
|
<li data-check="longueur">Au moins 8 caractères</li>
|
||||||
|
<li data-check="majuscule">Une majuscule</li>
|
||||||
|
<li data-check="minuscule">Une minuscule</li>
|
||||||
|
<li data-check="chiffre">Un chiffre</li>
|
||||||
|
<li data-check="special">Un caractère spécial (ex. ! ? # -)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Confirmer le nouveau mot de passe</label>
|
||||||
|
<div class="control"><input class="input" type="password" name="new_password_confirm" required autocomplete="new-password"></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-4">
|
||||||
|
<button type="submit" class="button primary">Changer le mot de passe</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" style="border:1px solid #b33;">
|
||||||
|
<h2 class="title is-5">Supprimer mon compte</h2>
|
||||||
|
<p class="hint mb-4">
|
||||||
|
{% if user.role != 'admin' %}
|
||||||
|
Action définitive et irréversible : ton compte ET ton jeu ({{ user.project_slug }}) seront supprimés, sans possibilité de récupération.
|
||||||
|
{% else %}
|
||||||
|
Action définitive et irréversible : ton compte sera supprimé (tes jeux existants ne sont pas touchés).
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<form method="post" action="{{ url_for('profile_delete') }}" autocomplete="off">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Mot de passe actuel</label>
|
||||||
|
<div class="control"><input class="input" type="password" name="current_password" required autocomplete="current-password"></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Tape "SUPPRIMER" pour confirmer</label>
|
||||||
|
<div class="control"><input class="input" type="text" name="confirm_text" required></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-4">
|
||||||
|
<button type="submit" class="button is-danger">Supprimer définitivement mon compte</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var MIN_SCORE_REQUIRED = {{ min_score_required }};
|
||||||
|
var PW_CHECKS = {
|
||||||
|
longueur: function (p) { return p.length >= 8; },
|
||||||
|
majuscule: function (p) { return /[A-Z]/.test(p); },
|
||||||
|
minuscule: function (p) { return /[a-z]/.test(p); },
|
||||||
|
chiffre: function (p) { return /\d/.test(p); },
|
||||||
|
special: function (p) { return /[^A-Za-z0-9]/.test(p); },
|
||||||
|
};
|
||||||
|
(function () {
|
||||||
|
var input = document.getElementById('password');
|
||||||
|
var fill = document.getElementById('pwFill');
|
||||||
|
var items = document.querySelectorAll('#pwChecklist li');
|
||||||
|
if (!input) return;
|
||||||
|
function update() {
|
||||||
|
var value = input.value || '';
|
||||||
|
var score = 0;
|
||||||
|
items.forEach(function (li) {
|
||||||
|
var ok = PW_CHECKS[li.dataset.check](value);
|
||||||
|
li.classList.toggle('ok', ok);
|
||||||
|
if (ok) score++;
|
||||||
|
});
|
||||||
|
var pct = (score / items.length) * 100;
|
||||||
|
fill.style.width = pct + '%';
|
||||||
|
fill.className = 'passwordStrengthFill' +
|
||||||
|
(score >= items.length ? ' strong' : score >= MIN_SCORE_REQUIRED ? ' good' : score >= 2 ? ' medium' : ' weak');
|
||||||
|
}
|
||||||
|
input.addEventListener('input', update);
|
||||||
|
update();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
+1
-1
@@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if g.get('current_user') %}
|
{% if g.get('current_user') %}
|
||||||
<div class="navbar-end" style="margin-left:auto; display:flex; align-items:center; gap:10px; padding-right:12px;">
|
<div class="navbar-end" style="margin-left:auto; display:flex; align-items:center; gap:10px; padding-right:12px;">
|
||||||
<span class="hint">{{ g.current_user.prenom }} {{ g.current_user.nom }}{{ ' (admin)' if g.current_user.role == 'admin' else '' }}</span>
|
<a href="{{ url_for('profile') }}" class="hint" style="text-decoration:underline;">{{ g.current_user.prenom }} {{ g.current_user.nom }}{{ ' (admin)' if g.current_user.role == 'admin' else '' }}</a>
|
||||||
<form method="post" action="{{ url_for('logout') }}"><button type="submit" class="button is-small">Déconnexion</button></form>
|
<form method="post" action="{{ url_for('logout') }}"><button type="submit" class="button is-small">Déconnexion</button></form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""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, _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)
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user