File size: 3,321 Bytes
5af9605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f5c422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5af9605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Sesiones firmadas por cookie.

El caso importante aquí es el NEGATIVO: `leer_sesion` capturaba `(BadSignature, Exception)`, de
modo que cualquier error de programación se convertía en «no hay sesión» y salía como un 401
silencioso, ocultando el bug real. Estos tests fijan qué se traga y qué debe propagarse.
"""

from __future__ import annotations

import pytest

from app.security import session as ses


def test_ida_y_vuelta():
    token = ses.firmar_sesion({"email": "vet@example.com"})
    sesion = ses.leer_sesion(token)
    assert sesion["email"] == "vet@example.com"


def test_toda_sesion_nace_identificada_y_fechada():
    """`jti` y `emitida_en` los pone `firmar_sesion`, no quien llama.

    Son lo que hace revocable una sesión: sin `jti` no hay nada que nombrar para cerrar UNA, y
    sin `emitida_en` no se pueden cortar TODAS las de una cuenta por fecha. Si se generaran en
    quien llama, una ruta podría emitir por descuido una sesión irrevocable.
    """
    a = ses.leer_sesion(ses.firmar_sesion({"email": "vet@example.com"}))
    b = ses.leer_sesion(ses.firmar_sesion({"email": "vet@example.com"}))

    assert a["jti"] and b["jti"]
    assert a["jti"] != b["jti"], "dos sesiones con el mismo jti se revocarían juntas"
    assert a["emitida_en"]


@pytest.mark.parametrize(
    "token",
    [
        None,
        "",
        "no-es-un-token",
        "a.b.c",
        "eyJlbWFpbCI6ICJhdGFjYW50ZUBleGFtcGxlLmNvbSJ9.falsificado.firma",
    ],
)
def test_tokens_invalidos_devuelven_none(token):
    """Firma inválida, token corrupto o ausente → sin sesión, sin excepción."""
    assert ses.leer_sesion(token) is None


def test_firma_con_otro_secreto_no_valida(monkeypatch):
    """Una cookie firmada con otro secreto (p. ej. el fallback de dev) no debe abrir sesión:
    es exactamente el escenario de suplantación que evita MORPHOS_SESSION_SECRET."""
    token = ses.firmar_sesion({"email": "vet@example.com"})

    ses.obtener_config.cache_clear()
    monkeypatch.setenv("MORPHOS_SESSION_SECRET", "otro-secreto-completamente-distinto-1234567890")
    try:
        assert ses.leer_sesion(token) is None
    finally:
        ses.obtener_config.cache_clear()


def test_sesion_caducada_devuelve_none(monkeypatch):
    token = ses.firmar_sesion({"email": "vet@example.com"})

    ses.obtener_config.cache_clear()
    # -1 y no 0: itsdangerous compara `edad > max_age`, así que un token recién creado (edad 0)
    # con max_age=0 todavía es válido. Con -1 se dispara SignatureExpired, subclase de BadData,
    # que es la rama que interesa comprobar.
    monkeypatch.setenv("MORPHOS_SESSION_MAX_AGE_S", "-1")
    try:
        assert ses.leer_sesion(token) is None
    finally:
        ses.obtener_config.cache_clear()


def test_los_errores_de_programacion_se_propagan(monkeypatch):
    """El fallo que motiva estos tests: un error interno NO puede disfrazarse de «sin sesión».
    Si `loads` revienta por un bug, debe propagarse para que se vea, no devolver None."""

    class SerializadorRoto:
        def loads(self, *_args, **_kwargs):
            raise RuntimeError("fallo interno, no es una cookie inválida")

    monkeypatch.setattr(ses, "_serializer", lambda: SerializadorRoto())
    with pytest.raises(RuntimeError):
        ses.leer_sesion("cualquier-token")