morphos / backend /tests /test_tenant_lab.py
Jose Salazar
Aislar por cl铆nica los resultados de analizador
1339cdc
Raw
History Blame Contribute Delete
5.76 kB
"""Aislamiento por cl铆nica de los resultados de analizador (ARCHITECTURE_REVIEW 搂2.1).
El almac茅n era un dict global sin due帽o: cualquier sesi贸n autenticada pod铆a leer la muestra de
cualquier cl铆nica, y `/api/lab/pendientes` enumeraba TODAS. Cada `muestra_id` abre en
`/api/lab/resultados` el panel completo m谩s las pistas de paciente (nombre de la mascota, raza,
sexo), as铆 que el aislamiento es lo 煤nico que separa a una cl铆nica de los datos de otra.
Dos cosas que estas pruebas fijan y no pueden relajarse: el tenant lo pone el SERVIDOR (de la API
key en la ingesta, de la cookie firmada en la lectura) y una muestra de otra cl铆nica se comporta
como inexistente, no como prohibida.
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from app.config import TENANT_POR_DEFECTO, obtener_config
from app.lab.almacen import almacen
from app.main import app
CLAVE_A = "clave-de-la-clinica-a"
CLAVE_B = "clave-de-la-clinica-b"
PAYLOAD = {
"muestra_id": "M-1",
"instrumento_id": "vetscan-1",
"fabricante": "Abaxis",
"observaciones": [{"codigo_prueba": "GLU", "valor": "5.0", "unidad": "mmol/L"}],
"momento": "2026-07-25T10:00:00Z",
}
@pytest.fixture
def cliente():
almacen._datos.clear()
with TestClient(app) as c:
yield c
almacen._datos.clear()
@pytest.fixture
def dos_clinicas(monkeypatch):
"""Dos dispositivos y dos cuentas, cada uno en su cl铆nica."""
cfg = obtener_config()
monkeypatch.setattr(cfg, "lab_api_keys", [f"clinica-a:{CLAVE_A}", f"clinica-b:{CLAVE_B}"])
monkeypatch.setattr(
cfg, "registro_allowlist", ["a@example.com=clinica-a", "b@example.com=clinica-b"]
)
monkeypatch.setattr(cfg, "lab_pendientes_habilitado", True)
def _ingerir(cliente, clave, muestra):
return cliente.post(
"/api/lab/ingesta",
json={**PAYLOAD, "muestra_id": muestra},
headers={"Authorization": f"Bearer {clave}"},
)
def _sesion(cliente, email):
"""Sesi贸n para ese email. La BD de pruebas se comparte, as铆 que si la cuenta ya existe de
otra prueba se entra en vez de darla de alta (el tenant qued贸 fijado al crearla)."""
r = cliente.post(
"/api/auth/registro",
json={"nombre": "V", "apellido": "Vet", "email": email, "password": "clave-segura-1"},
)
if r.status_code == 409:
r = cliente.post(
"/api/auth/login", json={"email": email, "password": "clave-segura-1"}
)
assert r.status_code == 200, r.text
return r
def test_una_clinica_no_ve_la_muestra_de_otra(cliente, dos_clinicas):
"""El hallazgo central de 搂2.1: cualquier sesi贸n le铆a cualquier muestra."""
assert _ingerir(cliente, CLAVE_A, "M-DE-A").status_code == 200
_sesion(cliente, "b@example.com")
r = cliente.get("/api/lab/resultados", params={"muestra": "M-DE-A"})
# 404 y no 403: existir o no en otra cl铆nica tampoco es informaci贸n que se d茅.
assert r.status_code == 404
def test_la_propia_clinica_si_la_ve(cliente, dos_clinicas):
"""El complemento obligatorio: aislar no puede romper el caso leg铆timo."""
assert _ingerir(cliente, CLAVE_A, "M-DE-A").status_code == 200
_sesion(cliente, "a@example.com")
r = cliente.get("/api/lab/resultados", params={"muestra": "M-DE-A"})
assert r.status_code == 200
assert r.json()["muestra_id"] == "M-DE-A"
def test_pendientes_solo_enumera_lo_propio(cliente, dos_clinicas):
"""`pendientes` era el volcado en una petici贸n: ahora s贸lo lista la cl铆nica de la sesi贸n."""
_ingerir(cliente, CLAVE_A, "M-DE-A")
_ingerir(cliente, CLAVE_B, "M-DE-B")
_sesion(cliente, "b@example.com")
r = cliente.get("/api/lab/pendientes")
assert r.status_code == 200
ids = [x["muestra_id"] for x in r.json()]
assert ids == ["M-DE-B"]
def test_el_puente_no_puede_declarar_su_clinica(cliente, dos_clinicas):
"""El tenant sale de la API key; si viniera del cuerpo, mentir bastar铆a para escribir en otra."""
r = cliente.post(
"/api/lab/ingesta",
json={**PAYLOAD, "muestra_id": "M-MENTIRA", "tenant": "clinica-b"},
headers={"Authorization": f"Bearer {CLAVE_A}"},
)
assert r.status_code == 200
_sesion(cliente, "b@example.com")
assert cliente.get("/api/lab/resultados", params={"muestra": "M-MENTIRA"}).status_code == 404
def test_clave_sin_clinica_declarada_cae_en_la_de_por_defecto(cliente, monkeypatch):
"""Compatibilidad: un despliegue de una sola cl铆nica no declara tenants y sigue funcionando."""
cfg = obtener_config()
monkeypatch.setattr(cfg, "lab_api_keys", ["clave-suelta"])
monkeypatch.setattr(cfg, "registro_abierto", True)
monkeypatch.setattr(cfg, "lab_pendientes_habilitado", True)
assert _ingerir(cliente, "clave-suelta", "M-SUELTA").status_code == 200
_sesion(cliente, "suelto@example.com")
r = cliente.get("/api/lab/resultados", params={"muestra": "M-SUELTA"})
assert r.status_code == 200
assert r.json()["tenant"] == TENANT_POR_DEFECTO
def test_clave_invalida_sigue_siendo_401(cliente, dos_clinicas):
assert _ingerir(cliente, "no-es-una-clave", "M-X").status_code == 401
def test_el_tenant_de_la_clave_se_resuelve_bien():
"""Unidad sobre el parseo `tenant:clave`, que es lo que sostiene todo lo anterior."""
cfg = obtener_config().model_copy(
update={"lab_api_keys": ["clinica-a:secreta-a", "suelta-sin-tenant"]}
)
assert cfg.tenant_de_clave_dispositivo("secreta-a") == "clinica-a"
assert cfg.tenant_de_clave_dispositivo("suelta-sin-tenant") == TENANT_POR_DEFECTO
assert cfg.tenant_de_clave_dispositivo("clinica-a") is None # el prefijo no es la clave
assert cfg.tenant_de_clave_dispositivo("") is None