From 7cc1fadf0f8edb3c25e52bbbd29711f2b992a8c0 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 3 Jul 2026 10:34:35 +0200 Subject: [PATCH] test(admin): add end-to-end Selenium coverage for the admin panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers anonymous → 404, non-admin → 404, and the full admin flow (list, detail, status change, journal) through a real login and a real running app. tests/users.test.sqlite is committed and shared by the whole Selenium session, so the test cleanup also resets the sqlite_sequence high-water marks that plain DELETEs don't roll back, keeping the file byte-stable across runs. This run additionally bakes in migration 0006_create_admin_actions (new admin_actions table), the first time any Selenium test has booted the real app since that migration was added — the same one-time process by which migrations 0001-0005 already ended up committed in this fixture. --- tests/admin/test_pages.py | 118 ++++++++++++++++++++++++++++++++++++++ tests/users.test.sqlite | Bin 106496 -> 106496 bytes 2 files changed, 118 insertions(+) create mode 100644 tests/admin/test_pages.py diff --git a/tests/admin/test_pages.py b/tests/admin/test_pages.py new file mode 100644 index 0000000..1e1f403 --- /dev/null +++ b/tests/admin/test_pages.py @@ -0,0 +1,118 @@ +import uuid + +from dash.testing.composite import DashComposite +from selenium.webdriver.support.ui import Select +from werkzeug.security import generate_password_hash + +from src.auth import db as auth_db +from src.subscriptions import db as sub_db + +PASSWORD = "s3cretpass!" + + +def _unique_email(prefix: str) -> str: + return f"{prefix}-{uuid.uuid4().hex[:8]}@ex.fr" + + +def _make_verified_user(email: str) -> int: + auth_db.init_schema() + uid = auth_db.create_user(email, generate_password_hash(PASSWORD)) + auth_db.set_email_verified(uid) + return uid + + +def _cleanup_user(user_id: int) -> None: + conn = auth_db.get_conn() + conn.execute("DELETE FROM admin_actions WHERE target_user_id = ?", (user_id,)) + conn.execute("DELETE FROM subscriptions WHERE user_id = ?", (user_id,)) + conn.execute("DELETE FROM subscriber_state WHERE user_id = ?", (user_id,)) + conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) + # tests/users.test.sqlite is committed to git and shared by the whole + # Selenium session (USERS_DB_PATH is set globally in pyproject.toml). + # Row DELETEs alone leave the file byte-diffed: AUTOINCREMENT tables + # record their high-water mark in sqlite_sequence, and that table is + # never rolled back by DELETE (by SQLite design). Reset it explicitly so + # a full test run leaves the committed file untouched (`git status` + # clean), matching the pre-existing convention for this file where + # users/subscriptions already sit at seq=0. + conn.execute( + "UPDATE sqlite_sequence SET seq = 0 " + "WHERE name IN ('users', 'subscriptions', 'admin_actions')" + ) + + +def _login(dash_duo: DashComposite, email: str): + dash_duo.driver.get(dash_duo.server_url + "/connexion") + dash_duo.wait_for_element("input[name=email]", timeout=8).send_keys(email) + dash_duo.driver.find_element("css selector", "input[name=password]").send_keys( + PASSWORD + ) + dash_duo.driver.find_element("css selector", "button[type=submit]").click() + + +def test_admin_anonymous_gets_404(dash_duo: DashComposite): + from src.app import app + + dash_duo.start_server(app) + dash_duo.driver.get(dash_duo.server_url + "/admin") + dash_duo.wait_for_text_to_equal("#admin-404-heading", "404", timeout=8) + + +def test_admin_non_admin_gets_404(dash_duo: DashComposite, monkeypatch): + from src.app import app + + monkeypatch.setenv("ADMIN_EMAIL", "admin-only@ex.fr") + email = _unique_email("regular") + uid = _make_verified_user(email) + try: + dash_duo.start_server(app) + _login(dash_duo, email) + dash_duo.driver.get(dash_duo.server_url + "/admin") + dash_duo.wait_for_text_to_equal("#admin-404-heading", "404", timeout=8) + finally: + _cleanup_user(uid) + + +def test_admin_full_flow(dash_duo: DashComposite, monkeypatch): + from src.app import app + + admin_email = _unique_email("admin") + monkeypatch.setenv("ADMIN_EMAIL", admin_email) + admin_uid = _make_verified_user(admin_email) + + target_email = _unique_email("target") + target_uid = _make_verified_user(target_email) + sub_db.init_schema() + _handle, sub_id = sub_db.create_pending(target_uid, "cust-e2e", "simple", 20.0) + sub_db.set_status(sub_id, "active") + + try: + dash_duo.start_server(app) + _login(dash_duo, admin_email) + + dash_duo.driver.get(dash_duo.server_url + "/admin") + dash_duo.wait_for_text_to_equal("h2", "Panneau admin", timeout=8) + assert target_email in dash_duo.driver.page_source + + dash_duo.driver.get(f"{dash_duo.server_url}/admin/user/{target_uid}") + dash_duo.wait_for_text_to_equal("h2", "Panneau admin", timeout=8) + assert target_email in dash_duo.driver.page_source + + select = Select( + dash_duo.driver.find_element("css selector", "select[name=status]") + ) + select.select_by_value("cancelled") + dash_duo.driver.find_element("css selector", "button[type=submit]").click() + + dash_duo.wait_for_text_to_equal( + ".alert-success", "Statut de l'abonnement mis à jour.", timeout=8 + ) + assert "cancelled" in dash_duo.driver.page_source + + dash_duo.driver.get(dash_duo.server_url + "/admin/journal") + dash_duo.wait_for_text_to_equal("h2", "Journal des actions admin", timeout=8) + assert "subscription_status_change" in dash_duo.driver.page_source + assert "active → cancelled" in dash_duo.driver.page_source + finally: + _cleanup_user(target_uid) + _cleanup_user(admin_uid) diff --git a/tests/users.test.sqlite b/tests/users.test.sqlite index e864fa7dd9491b0efd91f24a2a0ef54789706000..3808e15ad4ba204e768ebd6088e93b9e33f36da6 100644 GIT binary patch delta 582 zcmZoTz}9epZGyBQ4+8^(6cCF6F(VMmPt-AHD2f(DFV_$MiFY%J*G zpEx0lkC*=&12g||M*f@pPxy~-78KaY?<~N~tj>{`lADdnV(n8!N@GmQCgf@ zRLmg21e7c;O)5?<$}9j&0&U>}Vi+aBz`*|%WY0JLO#vJUEPQ`?f5NRrc7y?o5IgT1 z2L4_Ab^Jzr_xZZ`w0PfaY`n&6XT#2H?`vRSU>2WTl$uzQ8js=_BLgEdT?2Dn1LF__ z3oBzIDb zDxgRyXiQhgVpI)>il*i!X67h_xJHC1`1u2IsE?110wi+KMN1Nk(o;*~L9rE|nWEt7 o7vk#f8lr|_Lx@2HC#;mlOf&%0I&+J`~Uy| delta 212 zcmZoTz}9epZGyBQHv3uj>y3@q`8L0PCmOh2!GZBL|HKJ0jEu`U z0vHuGb0qxam*?EUz|9)N!1tAJ6E7>zIi7HC6Ruqx7OXK`-kf__gEltWux^iOWn9Ar IwD$);0HOmvzyJUM