Steph680/ormuz
0
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 