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