pollen-robotics/reachy_mini_central
1
1"""Unit tests for the HF token resolver dependency in ``app.py``.2 3The resolver is the first line of defence for authenticated endpoints;4a regression here can silently re-introduce token leaks or accept5malformed Authorization schemes.6 7No test infrastructure exists on this HF Space yet. Run manually with::8 9 pip install pytest httpx fastapi10 python -m pytest test_resolver.py -v11 12Keep these tests here so whoever adds CI can wire them up without13writing the suite from scratch.14"""15 16from __future__ import annotations17 18from typing import Optional19from unittest.mock import MagicMock20 21import pytest22from fastapi import HTTPException23 24from app import _resolve_hf_token25 26 27def _req(ip: str = "1.2.3.4") -> MagicMock:28 """Build a minimal ``Request``-shaped mock with .client.host."""29 request = MagicMock()30 request.client.host = ip31 return request32 33 34def _run(35 authorization: Optional[str] = None,36 token: str = "",37 ip: str = "1.2.3.4",38) -> str:39 """Invoke the resolver synchronously for a given input."""40 import asyncio41 42 return asyncio.run(_resolve_hf_token(_req(ip), authorization, token))43 44 45# ---- Header form (preferred) ----46 47def test_bearer_header_is_accepted():48 assert _run(authorization="Bearer hf_abc123") == "hf_abc123"49 50 51def test_bearer_header_case_insensitive_scheme():52 assert _run(authorization="bearer hf_abc123") == "hf_abc123"53 assert _run(authorization="BEARER hf_abc123") == "hf_abc123"54 55 56def test_bearer_header_trims_whitespace():57 assert _run(authorization="Bearer hf_abc123 ") == "hf_abc123"58 59 60def test_bearer_header_empty_token_is_rejected():61 """'Bearer' with nothing after must 401, not return ''."""62 with pytest.raises(HTTPException) as exc:63 _run(authorization="Bearer")64 assert exc.value.status_code == 40165 66 67def test_bearer_header_whitespace_only_token_is_rejected():68 with pytest.raises(HTTPException) as exc:69 _run(authorization="Bearer ")70 assert exc.value.status_code == 40171 72 73# ---- Unknown / malformed Authorization schemes ----74 75def test_basic_auth_scheme_is_rejected():76 """Basic auth must NOT leak through as a bare-string token."""77 with pytest.raises(HTTPException) as exc:78 _run(authorization="Basic dXNlcjpwdw==")79 assert exc.value.status_code == 40180 assert "Bearer" in exc.value.detail81 82 83def test_digest_scheme_is_rejected():84 with pytest.raises(HTTPException) as exc:85 _run(authorization="Digest username=foo")86 assert exc.value.status_code == 40187 88 89def test_bare_token_in_header_is_rejected():90 """A raw token with no scheme is not RFC 6750 shaped — reject it."""91 with pytest.raises(HTTPException) as exc:92 _run(authorization="hf_abc123")93 assert exc.value.status_code == 40194 95 96# ---- Query-string fallback ----97 98def test_query_token_is_accepted_and_deprecation_is_logged_once(caplog):99 # Clear the sampler state between tests — the module-level set100 # persists within a process.101 from app import _deprecation_warned_ips102 103 _deprecation_warned_ips.clear()104 with caplog.at_level("WARNING"):105 assert _run(token="hf_legacy") == "hf_legacy"106 warnings = [r for r in caplog.records if "deprecation" in r.message]107 assert len(warnings) == 1108 109 # Second call from the same IP does NOT re-log.110 caplog.clear()111 with caplog.at_level("WARNING"):112 assert _run(token="hf_legacy") == "hf_legacy"113 warnings = [r for r in caplog.records if "deprecation" in r.message]114 assert len(warnings) == 0115 116 # But a different IP triggers its own one-shot warning.117 caplog.clear()118 with caplog.at_level("WARNING"):119 assert _run(token="hf_legacy", ip="9.9.9.9") == "hf_legacy"120 warnings = [r for r in caplog.records if "deprecation" in r.message]121 assert len(warnings) == 1122 123 124# ---- Precedence (both forms present) ----125 126def test_header_takes_precedence_over_query():127 """If both are sent, the Authorization header wins and the query128 is ignored without logging deprecation (header users are compliant)."""129 assert _run(authorization="Bearer from_header", token="from_query") == "from_header"130 131 132# ---- Neither form present ----133 134def test_missing_both_raises_401():135 with pytest.raises(HTTPException) as exc:136 _run()137 assert exc.value.status_code == 401138 assert "Missing" in exc.value.detail139 