Spaces:
Sleeping
Sleeping
| """Versionado del esquema de la BD de usuarios. | |
| Antes era un único `CREATE TABLE IF NOT EXISTS`: creaba el esquema en una BD vacía y no hacía | |
| NADA sobre una existente. Añadir una columna era una operación manual sobre un fichero al que, | |
| en HF Spaces, nadie puede llegar. | |
| Lo que hay que sostener: los pasos se aplican una sola vez, se aplican en orden, y una BD creada | |
| ANTES de este mecanismo (versión 0 con las tablas del paso 1 ya dentro) se pone al día sin | |
| romperse. Ese último es el caso real de la instancia desplegada. | |
| """ | |
| from __future__ import annotations | |
| import sqlite3 | |
| import pytest | |
| from app import db | |
| def bd(tmp_path, monkeypatch): | |
| """BD temporal aislada de la del resto de la suite.""" | |
| ruta = tmp_path / "prueba.db" | |
| monkeypatch.setattr(db.obtener_config(), "db_path", ruta) | |
| return ruta | |
| def _version(ruta) -> int: | |
| con = sqlite3.connect(ruta) | |
| try: | |
| return con.execute("PRAGMA user_version").fetchone()[0] | |
| finally: | |
| con.close() | |
| def _tablas(ruta) -> set[str]: | |
| con = sqlite3.connect(ruta) | |
| try: | |
| filas = con.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() | |
| return {f[0] for f in filas} | |
| finally: | |
| con.close() | |
| def test_bd_nueva_queda_en_la_ultima_version(bd): | |
| db.inicializar_db() | |
| assert _version(bd) == len(db._MIGRACIONES) | |
| assert {"usuarios", "intentos_login", "resultados_lab"} <= _tablas(bd) | |
| def test_reejecutar_es_idempotente(bd): | |
| """Arrancar dos veces no debe reaplicar nada ni fallar.""" | |
| db.inicializar_db() | |
| db.inicializar_db() | |
| assert _version(bd) == len(db._MIGRACIONES) | |
| def test_bd_anterior_al_versionado_se_pone_al_dia(bd): | |
| """El caso real: tablas del paso 1 ya presentes y `user_version` en 0. | |
| Si los pasos no fueran reejecutables sobre lo ya existente, esto reventaría con | |
| «table usuarios already exists» y la app no arrancaría contra la BD desplegada. | |
| """ | |
| con = sqlite3.connect(bd) | |
| con.executescript(db._MIGRACIONES[0]) # esquema viejo, sin tocar user_version | |
| con.commit() | |
| con.close() | |
| assert _version(bd) == 0 | |
| db.inicializar_db() | |
| assert _version(bd) == len(db._MIGRACIONES) | |
| assert {"usuarios", "intentos_login", "resultados_lab"} <= _tablas(bd) | |
| def test_los_datos_sobreviven_a_la_migracion(bd): | |
| """Migrar no puede perder cuentas: es exactamente lo que se está intentando dejar de hacer.""" | |
| con = sqlite3.connect(bd) | |
| con.executescript(db._MIGRACIONES[0]) | |
| con.execute( | |
| "INSERT INTO usuarios (nombre, apellido, email, password) VALUES (?,?,?,?)", | |
| ("Ana", "Vet", "ana@example.com", "scrypt$x$y"), | |
| ) | |
| con.commit() | |
| con.close() | |
| db.inicializar_db() | |
| assert db.buscar_usuario("ana@example.com") is not None | |
| def test_se_aplica_un_paso_nuevo_sobre_una_bd_ya_migrada(bd, monkeypatch): | |
| """Comprueba el mecanismo, no un paso concreto: sin esto, sólo se probaría el estado final.""" | |
| db.inicializar_db() | |
| ya = len(db._MIGRACIONES) | |
| monkeypatch.setattr( | |
| db, "_MIGRACIONES", [*db._MIGRACIONES, "CREATE TABLE IF NOT EXISTS futura (x INTEGER);"] | |
| ) | |
| db.inicializar_db() | |
| assert _version(bd) == ya + 1 | |
| assert "futura" in _tablas(bd) | |
| def test_el_indice_del_throttle_existe(bd): | |
| """Paso 2: `intentos_recientes` filtra por email+ip+momento en cada intento de login.""" | |
| db.inicializar_db() | |
| con = sqlite3.connect(bd) | |
| try: | |
| indices = {f[0] for f in con.execute( | |
| "SELECT name FROM sqlite_master WHERE type='index'" | |
| ).fetchall()} | |
| finally: | |
| con.close() | |
| assert "idx_intentos_email_ip_momento" in indices | |