CoolFace
Apppublic

Steph680/ormuz

sourceHugging Faceupdated 2d agoView on Hugging Face
0likes
test_users.py192 linesDownload Raw Back to tests
1"""Tests des comptes (identifiant + mot de passe) et du contrôle d'accès."""2 3import asyncio4import sqlite35 6import pytest7from fastapi import HTTPException8 9from app import auth, db, users10 11 12def test_cycle_de_vie_dun_compte(tmp_path, monkeypatch):13    monkeypatch.setenv("DB_PATH", str(tmp_path / "t.db"))14 15    async def scenario():16        await db.init_db()17        user, code = await users.create_user("OHQ")18        assert user["role"] == "client"19 20        # Le code initial n'est pas stocké en clair.21        async with db.connect() as conn:22            cur = await conn.execute("SELECT password_hash FROM users WHERE id = ?", (user["id"],))23            (stored,) = await cur.fetchone()24        assert code not in stored and stored.startswith("scrypt$")25 26        # Mauvais identifiants -> refus.27        assert await users.login("OHQ", "faux") is None28        assert await users.login("Inconnu", code) is None29 30        # Bon code initial -> changement de mot de passe exigé, pas de session.31        res = await users.login("OHQ", code)32        assert res == {"must_change": True, "name": "OHQ"}33 34        # Mot de passe trop court refusé ; mauvais code refusé.35        with pytest.raises(ValueError):36            await users.change_password("OHQ", code, "court")37        assert await users.change_password("OHQ", "faux", "motdepasse-solide") is None38 39        # Changement valide -> session ouverte, l'ancien code ne marche plus.40        res = await users.change_password("OHQ", code, "motdepasse-solide")41        assert res["role"] == "client" and res["token"]42        assert (await users.resolve_token(res["token"]))["name"] == "OHQ"43        assert await users.login("OHQ", code) is None44 45        # Connexion normale ensuite.46        res2 = await users.login("OHQ", "motdepasse-solide")47        assert res2["must_change"] is False and res2["token"]48 49        # Déconnexion volontaire : la session tombe, le mot de passe reste.50        res3 = await users.login("OHQ", "motdepasse-solide")51        assert await users.logout(res3["token"]) is True52        assert await users.resolve_token(res3["token"]) is None53        assert await users.logout("jeton-inconnu") is False54        assert (await users.login("OHQ", "motdepasse-solide"))["token"]55 56        # Révocation : sessions coupées ; réactivation : le mot de passe revit.57        assert await users.set_active(user["id"], False)58        assert await users.resolve_token(res2["token"]) is None59        assert await users.login("OHQ", "motdepasse-solide") is None60        assert await users.set_active(user["id"], True)61        assert (await users.login("OHQ", "motdepasse-solide"))["token"]62 63        # Doublon (insensible à la casse) et entrées invalides refusés.64        with pytest.raises(sqlite3.IntegrityError):65            await users.create_user("ohq")66        with pytest.raises(ValueError):67            await users.create_user("  ")68        with pytest.raises(ValueError):69            await users.create_user("Y", role="super")70 71    asyncio.run(scenario())72 73 74def test_reset_code(tmp_path, monkeypatch):75    monkeypatch.setenv("DB_PATH", str(tmp_path / "t.db"))76 77    async def scenario():78        await db.init_db()79        user, code = await users.create_user("Stéph", role="admin")80        res = await users.change_password("Stéph", code, "motdepasse-solide")81        token = res["token"]82 83        # Réinitialisation : sessions coupées, nouveau code à usage initial.84        nouveau = await users.reset_code(user["id"])85        assert nouveau and nouveau != code86        assert await users.resolve_token(token) is None87        assert await users.login("Stéph", "motdepasse-solide") is None88        assert (await users.login("Stéph", nouveau)) == {"must_change": True, "name": "Stéph"}89        assert await users.reset_code(99999) is None90 91    asyncio.run(scenario())92 93 94def test_controle_dacces(tmp_path, monkeypatch):95    monkeypatch.setenv("DB_PATH", str(tmp_path / "t.db"))96    monkeypatch.setenv("ACCESS_KEY", "cle-amorcage")97 98    async def scenario():99        await db.init_db()100        _, code_c = await users.create_user("Client A")101        _, code_a = await users.create_user("Admin B", role="admin")102        tok_c = (await users.change_password("Client A", code_c, "motdepasse-c"))["token"]103        tok_a = (await users.change_password("Admin B", code_a, "motdepasse-a"))["token"]104 105        # ACCESS_KEY du Space : accès total (amorçage).106        await auth.require_access_key("cle-amorcage")107        await auth.require_admin("cle-amorcage")108 109        # Session client : dashboard oui, admin non (403).110        await auth.require_access_key(tok_c)111        with pytest.raises(HTTPException) as e:112            await auth.require_admin(tok_c)113        assert e.value.status_code == 403114 115        # Session d'un compte admin : les deux.116        await auth.require_admin(tok_a)117 118        # Jeton invalide ou absent : 401.119        for bad in ("", "n-importe-quoi"):120            with pytest.raises(HTTPException) as e:121                await auth.require_access_key(bad)122            assert e.value.status_code == 401123 124    asyncio.run(scenario())125 126 127def test_frequentation(tmp_path, monkeypatch):128    monkeypatch.setenv("DB_PATH", str(tmp_path / "t.db"))129    monkeypatch.setenv("ACCESS_KEY", "cle-amorcage")130 131    async def scenario():132        await db.init_db()133        _, code = await users.create_user("Client A")134        tok = (await users.change_password("Client A", code, "motdepasse-c"))["token"]135        await auth.require_access_key(tok)136        await auth.require_access_key(tok)137        await auth.require_access_key("cle-amorcage")138        par_user = {r["user"]: r["hits"] for r in await users.usage(days=1)}139        assert par_user["Client A"] == 2140        assert par_user["(ACCESS_KEY)"] == 1141 142    asyncio.run(scenario())143 144 145def test_migration_v4_conserve_les_comptes(tmp_path, monkeypatch):146    """Une base à l'ancien schéma (key_hash) est migrée : comptes conservés,147    mot de passe à générer par l'admin."""148    monkeypatch.setenv("DB_PATH", str(tmp_path / "t.db"))149 150    async def scenario():151        # Fabrique une base à l'ancien schéma v3.152        async with db.connect() as conn:153            await conn.execute("""154                CREATE TABLE users (155                  id INTEGER PRIMARY KEY AUTOINCREMENT,156                  name TEXT UNIQUE NOT NULL,157                  key_hash TEXT UNIQUE NOT NULL,158                  role TEXT NOT NULL DEFAULT 'client',159                  active INTEGER NOT NULL DEFAULT 1,160                  created_at TEXT,161                  last_seen TEXT162                )""")163            await conn.execute(164                "INSERT INTO users (name, key_hash, role, active, created_at)"165                " VALUES ('OHQ', 'vieux-hash', 'client', 1, '2026-07-19T00:00:00')")166            await conn.execute("PRAGMA user_version = 3")167            await conn.commit()168        await db.init_db()169        liste = await users.list_users()170        assert len(liste) == 1171        assert liste[0]["name"] == "OHQ"172        assert liste[0]["etat_mdp"] == "a_generer"173        # Pas connectable tant que l'admin n'a pas généré de code.174        assert await users.login("OHQ", "vieux-hash") is None175        # Après génération d'un code, le parcours normal fonctionne.176        code = await users.reset_code(liste[0]["id"])177        assert (await users.login("OHQ", code))["must_change"] is True178 179    asyncio.run(scenario())180 181 182def test_sans_access_key_mode_dev_ouvert(tmp_path, monkeypatch):183    monkeypatch.setenv("DB_PATH", str(tmp_path / "t.db"))184    monkeypatch.delenv("ACCESS_KEY", raising=False)185 186    async def scenario():187        await db.init_db()188        await auth.require_access_key("")189        await auth.require_admin("")190 191    asyncio.run(scenario())192