morphos / backend /tests /test_migraciones_db.py
Jose Salazar
Esquema versionado y BD de usuarios en volumen persistente si lo hay
b90d0d2
Raw
History Blame Contribute Delete
3.67 kB
"""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
@pytest.fixture
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