feat: support upstream token refresh
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.services import upstream_client
|
||||
from app.services.upstream_client import UpstreamClient
|
||||
|
||||
|
||||
class FakeHttpClient:
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.calls = []
|
||||
|
||||
def request(self, method: str, url: str, **kwargs):
|
||||
request = httpx.Request(method, url)
|
||||
self.calls.append({
|
||||
"method": method,
|
||||
"path": request.url.path,
|
||||
"headers": kwargs.get("headers") or {},
|
||||
"json": kwargs.get("json"),
|
||||
})
|
||||
return self.handler(request, kwargs)
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _install_fake_client(monkeypatch, handler) -> FakeHttpClient:
|
||||
fake = FakeHttpClient(handler)
|
||||
monkeypatch.setattr(upstream_client.httpx, "Client", lambda timeout=None: fake)
|
||||
return fake
|
||||
|
||||
|
||||
def _response(request: httpx.Request, status_code: int, payload=None) -> httpx.Response:
|
||||
return httpx.Response(status_code, json=payload, request=request)
|
||||
|
||||
|
||||
def test_sub2api_bearer_refreshes_on_401_and_retries(monkeypatch):
|
||||
updates: list[dict] = []
|
||||
|
||||
def handler(request: httpx.Request, kwargs):
|
||||
if request.url.path == "/api/v1/groups/available":
|
||||
auth = kwargs["headers"].get("Authorization")
|
||||
if auth == "Bearer old-access":
|
||||
return _response(request, 401, {"detail": "expired"})
|
||||
assert auth == "Bearer new-access"
|
||||
return _response(request, 200, [{"id": "g1", "name": "G1"}])
|
||||
if request.url.path == "/api/v1/auth/refresh":
|
||||
assert kwargs["json"] == {"refresh_token": "old-refresh"}
|
||||
return _response(request, 200, {
|
||||
"access_token": "new-access",
|
||||
"refresh_token": "new-refresh",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
raise AssertionError(f"unexpected request: {request.method} {request.url}")
|
||||
|
||||
fake = _install_fake_client(monkeypatch, handler)
|
||||
client = UpstreamClient(
|
||||
base_url="http://sub2api.local",
|
||||
api_prefix="/api/v1",
|
||||
auth_type="bearer",
|
||||
auth_config={"token": "old-access", "refresh_token": "old-refresh"},
|
||||
on_auth_config_update=updates.append,
|
||||
)
|
||||
|
||||
groups = client.get_available_groups("/groups/available")
|
||||
|
||||
assert groups == [{"id": "g1", "name": "G1"}]
|
||||
assert [(c["method"], c["path"]) for c in fake.calls] == [
|
||||
("GET", "/api/v1/groups/available"),
|
||||
("POST", "/api/v1/auth/refresh"),
|
||||
("GET", "/api/v1/groups/available"),
|
||||
]
|
||||
assert updates[-1]["token"] == "new-access"
|
||||
assert updates[-1]["refresh_token"] == "new-refresh"
|
||||
assert updates[-1]["expires_in"] == 3600
|
||||
|
||||
|
||||
def test_sub2api_bearer_without_refresh_token_does_not_refresh(monkeypatch):
|
||||
def handler(request: httpx.Request, kwargs):
|
||||
assert request.url.path == "/api/v1/groups/available"
|
||||
return _response(request, 401, {"detail": "expired"})
|
||||
|
||||
fake = _install_fake_client(monkeypatch, handler)
|
||||
client = UpstreamClient(
|
||||
base_url="http://sub2api.local",
|
||||
api_prefix="/api/v1",
|
||||
auth_type="bearer",
|
||||
auth_config={"token": "old-access"},
|
||||
)
|
||||
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
client.get_available_groups("/groups/available")
|
||||
|
||||
assert [(c["method"], c["path"]) for c in fake.calls] == [
|
||||
("GET", "/api/v1/groups/available"),
|
||||
]
|
||||
|
||||
|
||||
def test_non_sub2api_bearer_does_not_refresh(monkeypatch):
|
||||
def handler(request: httpx.Request, kwargs):
|
||||
assert request.url.path == "/groups/available"
|
||||
return _response(request, 401, {"detail": "expired"})
|
||||
|
||||
fake = _install_fake_client(monkeypatch, handler)
|
||||
client = UpstreamClient(
|
||||
base_url="http://plain-bearer.local",
|
||||
api_prefix="",
|
||||
auth_type="bearer",
|
||||
auth_config={"token": "old-access", "refresh_token": "old-refresh"},
|
||||
)
|
||||
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
client.get_available_groups("/groups/available")
|
||||
|
||||
assert [(c["method"], c["path"]) for c in fake.calls] == [
|
||||
("GET", "/groups/available"),
|
||||
]
|
||||
|
||||
|
||||
def test_login_password_saves_sub2api_refresh_token(monkeypatch):
|
||||
updates: list[dict] = []
|
||||
|
||||
def handler(request: httpx.Request, kwargs):
|
||||
assert request.url.path == "/api/v1/auth/login"
|
||||
assert kwargs["json"] == {"email": "user@example.com", "password": "secret"}
|
||||
return _response(request, 200, {
|
||||
"access_token": "login-access",
|
||||
"refresh_token": "login-refresh",
|
||||
"expires_in": 1800,
|
||||
})
|
||||
|
||||
_install_fake_client(monkeypatch, handler)
|
||||
client = UpstreamClient(
|
||||
base_url="http://sub2api.local",
|
||||
api_prefix="/api/v1",
|
||||
auth_type="login_password",
|
||||
auth_config={"email": "user@example.com", "password": "secret"},
|
||||
on_auth_config_update=updates.append,
|
||||
)
|
||||
|
||||
client.login()
|
||||
|
||||
assert client._headers()["Authorization"] == "Bearer login-access"
|
||||
assert updates[-1]["token"] == "login-access"
|
||||
assert updates[-1]["refresh_token"] == "login-refresh"
|
||||
assert updates[-1]["expires_in"] == 1800
|
||||
|
||||
|
||||
def test_login_password_empty_prefix_defaults_to_user_api_login(monkeypatch):
|
||||
def handler(request: httpx.Request, kwargs):
|
||||
assert request.url.path == "/api/user/login"
|
||||
assert kwargs["json"] == {"username": "admin", "password": "secret"}
|
||||
return _response(request, 200, {
|
||||
"token": "nox-access",
|
||||
"user": {"id": 9},
|
||||
})
|
||||
|
||||
_install_fake_client(monkeypatch, handler)
|
||||
client = UpstreamClient(
|
||||
base_url="http://nox.local",
|
||||
api_prefix="",
|
||||
auth_type="login_password",
|
||||
auth_config={"email": "admin", "password": "secret"},
|
||||
)
|
||||
|
||||
client.login()
|
||||
|
||||
headers = client._headers()
|
||||
assert headers["Authorization"] == "Bearer nox-access"
|
||||
assert headers["Nox-Api-User"] == "9"
|
||||
assert headers["New-Api-User"] == "9"
|
||||
Reference in New Issue
Block a user