Spaces:
Sleeping
Sleeping
File size: 5,761 Bytes
1339cdc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | """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
|