eb8dbf66a1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
217 lines
6.4 KiB
Python
217 lines
6.4 KiB
Python
"""Tests para hoppscotch_login.
|
|
|
|
Deterministas: monkeypatchean requests.Session para no tocar la red. Simulan el
|
|
flujo magic link completo (signin -> mailpit list -> mailpit message -> verify)
|
|
y verifican que se devuelven los JWT, asi como los caminos de error.
|
|
"""
|
|
|
|
import sys
|
|
|
|
import infra.hoppscotch_login # noqa: F401 (registra el submodulo en sys.modules)
|
|
|
|
# El __init__ del paquete rebinds el nombre `hoppscotch_login` a la funcion,
|
|
# que sombrea el submodulo. Recuperamos el submodulo real desde sys.modules
|
|
# para monkeypatchear su simbolo `requests`.
|
|
mod = sys.modules["infra.hoppscotch_login"]
|
|
|
|
|
|
class _FakeResponse:
|
|
"""Respuesta HTTP mockeada minima: status_code, json(), text."""
|
|
|
|
def __init__(self, status_code=200, json_data=None, text=""):
|
|
self.status_code = status_code
|
|
self._json = json_data
|
|
self.text = text
|
|
|
|
def json(self):
|
|
if self._json is None:
|
|
raise ValueError("no json")
|
|
return self._json
|
|
|
|
|
|
class _FakeCookies:
|
|
def __init__(self, store):
|
|
self._store = store
|
|
|
|
def get(self, name):
|
|
return self._store.get(name)
|
|
|
|
|
|
class _FakeSession:
|
|
"""Session mockeada: despacha por (method, path) a respuestas predefinidas."""
|
|
|
|
def __init__(self, routes, cookie_store):
|
|
self._routes = routes
|
|
self.cookies = _FakeCookies(cookie_store)
|
|
self.calls = []
|
|
|
|
def _dispatch(self, method, url, **kwargs):
|
|
self.calls.append((method, url, kwargs))
|
|
for (m, fragment), resp in self._routes.items():
|
|
if m == method and fragment in url:
|
|
return resp
|
|
raise AssertionError(f"unexpected {method} {url}")
|
|
|
|
def post(self, url, **kwargs):
|
|
return self._dispatch("POST", url, **kwargs)
|
|
|
|
def get(self, url, **kwargs):
|
|
return self._dispatch("GET", url, **kwargs)
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
def _install_session(monkeypatch, routes, cookie_store):
|
|
session = _FakeSession(routes, cookie_store)
|
|
monkeypatch.setattr(mod.requests, "Session", lambda: session)
|
|
return session
|
|
|
|
|
|
def test_golden_login_devuelve_tokens(monkeypatch):
|
|
routes = {
|
|
("POST", "/v1/auth/signin"): _FakeResponse(
|
|
201, {"deviceIdentifier": "dev-123"}
|
|
),
|
|
("GET", "/api/v1/messages"): _FakeResponse(
|
|
200,
|
|
{
|
|
"messages": [
|
|
{
|
|
"ID": "msg-1",
|
|
"Subject": "Sign in to Hoppscotch",
|
|
"To": [{"Address": "admin@example.com"}],
|
|
}
|
|
]
|
|
},
|
|
),
|
|
("GET", "/api/v1/message/msg-1"): _FakeResponse(
|
|
200,
|
|
{
|
|
"Text": "Click here",
|
|
"HTML": (
|
|
"<a href='http://localhost:3170/?token="
|
|
"eyJhbGciOi.JhbGci_Q-zz'>Sign in</a>"
|
|
),
|
|
},
|
|
),
|
|
("POST", "/v1/auth/verify"): _FakeResponse(200, {"ok": True}),
|
|
}
|
|
_install_session(
|
|
monkeypatch,
|
|
routes,
|
|
{"access_token": "ACCESS-JWT", "refresh_token": "REFRESH-JWT"},
|
|
)
|
|
|
|
result = mod.hoppscotch_login("admin@example.com")
|
|
|
|
assert result["status"] == "ok"
|
|
assert result["access_token"] == "ACCESS-JWT"
|
|
assert result["refresh_token"] == "REFRESH-JWT"
|
|
assert result["email"] == "admin@example.com"
|
|
|
|
|
|
def test_verify_recibe_token_extraido_y_device_identifier(monkeypatch):
|
|
routes = {
|
|
("POST", "/v1/auth/signin"): _FakeResponse(
|
|
201, {"deviceIdentifier": "dev-xyz"}
|
|
),
|
|
("GET", "/api/v1/messages"): _FakeResponse(
|
|
200,
|
|
{
|
|
"messages": [
|
|
{
|
|
"ID": "m9",
|
|
"Subject": "Sign in",
|
|
"To": [{"Address": "admin@example.com"}],
|
|
}
|
|
]
|
|
},
|
|
),
|
|
("GET", "/api/v1/message/m9"): _FakeResponse(
|
|
200,
|
|
{"Text": "verify at ?token=abc.DEF-123_456", "HTML": ""},
|
|
),
|
|
("POST", "/v1/auth/verify"): _FakeResponse(200, {}),
|
|
}
|
|
session = _install_session(
|
|
monkeypatch, routes, {"access_token": "A", "refresh_token": "R"}
|
|
)
|
|
|
|
result = mod.hoppscotch_login("admin@example.com")
|
|
assert result["status"] == "ok"
|
|
|
|
# El POST a verify llevo el token extraido del correo + el deviceIdentifier.
|
|
verify_call = next(
|
|
c for c in session.calls if c[0] == "POST" and "verify" in c[1]
|
|
)
|
|
sent = verify_call[2]["json"]
|
|
assert sent["token"] == "abc.DEF-123_456"
|
|
assert sent["deviceIdentifier"] == "dev-xyz"
|
|
|
|
|
|
def test_error_signin_no_201(monkeypatch):
|
|
routes = {
|
|
("POST", "/v1/auth/signin"): _FakeResponse(
|
|
500, None, text="boom"
|
|
),
|
|
}
|
|
_install_session(monkeypatch, routes, {})
|
|
|
|
result = mod.hoppscotch_login("admin@example.com")
|
|
assert result["status"] == "error"
|
|
assert "signin returned 500" in result["error"]
|
|
|
|
|
|
def test_error_correo_no_encontrado(monkeypatch):
|
|
routes = {
|
|
("POST", "/v1/auth/signin"): _FakeResponse(
|
|
201, {"deviceIdentifier": "d"}
|
|
),
|
|
("GET", "/api/v1/messages"): _FakeResponse(
|
|
200,
|
|
{
|
|
"messages": [
|
|
{
|
|
"ID": "x",
|
|
"Subject": "Newsletter",
|
|
"To": [{"Address": "other@example.com"}],
|
|
}
|
|
]
|
|
},
|
|
),
|
|
}
|
|
_install_session(monkeypatch, routes, {})
|
|
|
|
result = mod.hoppscotch_login("admin@example.com")
|
|
assert result["status"] == "error"
|
|
assert "no 'Sign in' email" in result["error"]
|
|
|
|
|
|
def test_error_token_no_en_correo(monkeypatch):
|
|
routes = {
|
|
("POST", "/v1/auth/signin"): _FakeResponse(
|
|
201, {"deviceIdentifier": "d"}
|
|
),
|
|
("GET", "/api/v1/messages"): _FakeResponse(
|
|
200,
|
|
{
|
|
"messages": [
|
|
{
|
|
"ID": "m",
|
|
"Subject": "Sign in",
|
|
"To": [{"Address": "admin@example.com"}],
|
|
}
|
|
]
|
|
},
|
|
),
|
|
("GET", "/api/v1/message/m"): _FakeResponse(
|
|
200, {"Text": "no token here", "HTML": "<p>nada</p>"}
|
|
),
|
|
}
|
|
_install_session(monkeypatch, routes, {})
|
|
|
|
result = mod.hoppscotch_login("admin@example.com")
|
|
assert result["status"] == "error"
|
|
assert "token not found" in result["error"]
|