import sys from types import ModuleType from typing import Any import pytest from fastapi import HTTPException from app.core.config import get_settings from app.modules.feishu.client import FeishuClient from app.modules.feishu.errors import FeishuAPIError class _Response: def __init__(self, payload: Any, status_code: int = 200): self.payload = payload self.status_code = status_code def json(self) -> Any: if isinstance(self.payload, Exception): raise self.payload return self.payload class _HTTPClient: responses: list[_Response] = [] requests: list[dict[str, Any]] = [] def __init__(self, *, timeout: int): self.timeout = timeout def __enter__(self) -> "_HTTPClient": return self def __exit__(self, *_: Any) -> None: return None def post(self, url: str, **kwargs: Any) -> _Response: self.requests.append({"url": url, "timeout": self.timeout, **kwargs}) return self.responses.pop(0) @pytest.fixture(autouse=True) def _reset_settings_and_http(monkeypatch: pytest.MonkeyPatch) -> None: _HTTPClient.responses = [] _HTTPClient.requests = [] monkeypatch.setattr("app.modules.feishu.client.httpx.Client", _HTTPClient) get_settings.cache_clear() yield get_settings.cache_clear() def _configure( monkeypatch: pytest.MonkeyPatch, *, app_type: str, app_ticket: str = "", ) -> None: monkeypatch.setenv("FEISHU_BASE_URL", "https://open.feishu.test/open-apis") monkeypatch.setenv("FEISHU_APP_ID", "cli-test") monkeypatch.setenv("FEISHU_APP_SECRET", "app-secret-value") monkeypatch.setenv("FEISHU_APP_TYPE", app_type) monkeypatch.setenv("FEISHU_APP_TICKET", app_ticket) monkeypatch.delenv("FEISHU_DEFAULT_TENANT_KEY", raising=False) get_settings.cache_clear() def test_self_app_uses_internal_tenant_token_request( monkeypatch: pytest.MonkeyPatch, ) -> None: _configure(monkeypatch, app_type="self") _HTTPClient.responses = [ _Response( { "code": 0, "tenant_access_token": "self-tenant-token", "expire": 7200, } ) ] token = FeishuClient()._get_tenant_access_token("ignored-tenant") assert token == "self-tenant-token" assert len(_HTTPClient.requests) == 1 request = _HTTPClient.requests[0] assert request["url"].endswith("/auth/v3/tenant_access_token/internal") assert request["json"] == { "app_id": "cli-test", "app_secret": "app-secret-value", } def test_store_app_caches_app_token_and_isolates_tenant_tokens( monkeypatch: pytest.MonkeyPatch, ) -> None: _configure(monkeypatch, app_type="store", app_ticket="latest-ticket") _HTTPClient.responses = [ _Response({"code": 0, "app_access_token": "app-token", "expire": 7200}), _Response( {"code": 0, "tenant_access_token": "tenant-a-token", "expire": 7200} ), _Response( {"code": 0, "tenant_access_token": "tenant-b-token", "expire": 7200} ), ] client = FeishuClient() tenant_a = client._get_tenant_access_token("tenant-a") tenant_b = client._get_tenant_access_token("tenant-b") tenant_a_again = client._get_tenant_access_token("tenant-a") assert (tenant_a, tenant_b, tenant_a_again) == ( "tenant-a-token", "tenant-b-token", "tenant-a-token", ) assert len(_HTTPClient.requests) == 3 app_request, tenant_a_request, tenant_b_request = _HTTPClient.requests assert app_request["url"].endswith("/auth/v3/app_access_token") assert app_request["json"] == { "app_id": "cli-test", "app_secret": "app-secret-value", "app_ticket": "latest-ticket", } assert tenant_a_request["url"].endswith("/auth/v3/tenant_access_token") assert tenant_a_request["json"] == { "app_access_token": "app-token", "tenant_key": "tenant-a", } assert tenant_b_request["json"] == { "app_access_token": "app-token", "tenant_key": "tenant-b", } def test_store_app_prefers_persisted_ticket( monkeypatch: pytest.MonkeyPatch, ) -> None: _configure(monkeypatch, app_type="store", app_ticket="environment-ticket") class _TicketService: def __init__(self, db: object): assert db is database def get_ticket(self, app_id: str) -> str: assert app_id == "cli-test" return "persisted-ticket" database = object() ticket_module = ModuleType("app.modules.feishu.app_tickets") ticket_module.FeishuAppTicketService = _TicketService monkeypatch.setitem(sys.modules, ticket_module.__name__, ticket_module) _HTTPClient.responses = [ _Response({"code": 0, "app_access_token": "app-token", "expire": 7200}), _Response( {"code": 0, "tenant_access_token": "tenant-token", "expire": 7200} ), ] FeishuClient(database)._get_tenant_access_token("tenant-a") assert _HTTPClient.requests[0]["json"]["app_ticket"] == "persisted-ticket" def test_store_app_requires_tenant_key_before_request( monkeypatch: pytest.MonkeyPatch, ) -> None: _configure(monkeypatch, app_type="store", app_ticket="latest-ticket") with pytest.raises(HTTPException) as exc_info: FeishuClient()._get_tenant_access_token() assert exc_info.value.status_code == 400 assert exc_info.value.detail == "tenant_key is required for Feishu store apps" assert _HTTPClient.requests == [] def test_store_app_requires_ticket_before_request( monkeypatch: pytest.MonkeyPatch, ) -> None: _configure(monkeypatch, app_type="store") with pytest.raises(HTTPException) as exc_info: FeishuClient()._get_tenant_access_token("tenant-a") assert exc_info.value.status_code == 503 assert exc_info.value.detail == "Feishu store app ticket is not available" assert _HTTPClient.requests == [] @pytest.mark.parametrize( ("status_code", "retryable"), [(401, False), (429, True), (500, True)], ) def test_token_http_errors_are_classified_without_exposing_secrets( monkeypatch: pytest.MonkeyPatch, status_code: int, retryable: bool, ) -> None: _configure(monkeypatch, app_type="store", app_ticket="secret-ticket-value") _HTTPClient.responses = [ _Response( { "code": 1, "app_ticket": "secret-ticket-value", "tenant_access_token": "secret-token-value", }, status_code=status_code, ) ] with pytest.raises(FeishuAPIError) as exc_info: FeishuClient()._get_tenant_access_token("tenant-a") error = exc_info.value assert error.retryable is retryable serialized = f"{error.detail} {error.provider_response}" assert "secret-ticket-value" not in serialized assert "secret-token-value" not in serialized @pytest.mark.parametrize( ("payload", "retryable"), [ ({"code": 99991400, "msg": "rate limited"}, True), ({"code": 10003, "msg": "invalid app credentials"}, False), ], ) def test_token_business_errors_are_classified( monkeypatch: pytest.MonkeyPatch, payload: dict[str, Any], retryable: bool, ) -> None: _configure(monkeypatch, app_type="store", app_ticket="latest-ticket") _HTTPClient.responses = [_Response(payload)] with pytest.raises(FeishuAPIError) as exc_info: FeishuClient()._get_tenant_access_token("tenant-a") assert exc_info.value.provider_code == payload["code"] assert exc_info.value.retryable is retryable def test_send_text_uses_the_requested_tenant_token( monkeypatch: pytest.MonkeyPatch, ) -> None: _configure(monkeypatch, app_type="store", app_ticket="latest-ticket") _HTTPClient.responses = [ _Response({"code": 0, "app_access_token": "app-token", "expire": 7200}), _Response( {"code": 0, "tenant_access_token": "tenant-a-token", "expire": 7200} ), _Response({"code": 0, "data": {"message_id": "om-message"}}), ] result = FeishuClient().send_text( "hello", receive_id="ou-user", receive_id_type="open_id", uuid="stable-uuid", tenant_key="tenant-a", ) assert result["code"] == 0 message_request = _HTTPClient.requests[2] assert message_request["headers"]["Authorization"] == "Bearer tenant-a-token" assert message_request["json"]["uuid"] == "stable-uuid"