import json from django.utils import timezone import pytest from review_agent.models import FeishuAccessTokenCache from review_agent.notifications.context import NotificationContext from review_agent.notifications.feishu_message_api import send_personal_message from review_agent.notifications.feishu_token import app_id_hash, get_tenant_access_token from review_agent.notifications.message_builder import build_feishu_post_message from review_agent.notifications.recipient import resolve_configured_personal_recipient pytestmark = pytest.mark.django_db class FakeResponse: def __init__(self, payload, status_code=200): self.payload = payload self.status_code = status_code self.text = json.dumps(payload, ensure_ascii=False) def json(self): return self.payload def test_token_service_fetches_and_caches(monkeypatch, settings): settings.FEISHU_APP_ID = "cli_a" settings.FEISHU_APP_SECRET = "secret" calls = [] def fake_post(*args, **kwargs): calls.append(kwargs) return FakeResponse({"code": 0, "tenant_access_token": "tenant-token", "expire": 7200}) monkeypatch.setattr("review_agent.notifications.feishu_token.httpx.post", fake_post) first = get_tenant_access_token() second = get_tenant_access_token() assert first.ok assert second.tenant_access_token == "tenant-token" assert len(calls) == 1 assert FeishuAccessTokenCache.objects.get(app_id_hash=app_id_hash("cli_a")).is_valid() def test_token_service_refreshes_expired_cache(monkeypatch, settings): settings.FEISHU_APP_ID = "cli_a" settings.FEISHU_APP_SECRET = "secret" FeishuAccessTokenCache.objects.create( app_id_hash=app_id_hash("cli_a"), tenant_access_token="old", expires_at=timezone.now() - timezone.timedelta(minutes=1), ) monkeypatch.setattr( "review_agent.notifications.feishu_token.httpx.post", lambda *args, **kwargs: FakeResponse({"code": 0, "tenant_access_token": "new", "expire": 7200}), ) assert get_tenant_access_token().tenant_access_token == "new" def test_token_service_returns_error_for_api_failure(monkeypatch, settings): settings.FEISHU_APP_ID = "cli_a" settings.FEISHU_APP_SECRET = "secret" monkeypatch.setattr( "review_agent.notifications.feishu_token.httpx.post", lambda *args, **kwargs: FakeResponse({"code": 1, "msg": "bad secret"}), ) result = get_tenant_access_token() assert not result.ok assert result.error_message == "bad secret" def test_recipient_prefers_open_id(settings): settings.FEISHU_DEFAULT_USER_OPEN_ID = "ou_xxx" settings.FEISHU_DEFAULT_USER_ID = "user_xxx" settings.FEISHU_DEFAULT_TARGET_NAME = "负责人" target = resolve_configured_personal_recipient() assert target.ok assert target.identifier_type == "open_id" assert target.identifier_value == "ou_xxx" def test_recipient_uses_user_id_when_open_id_missing(settings): settings.FEISHU_DEFAULT_USER_OPEN_ID = "" settings.FEISHU_DEFAULT_USER_ID = "user_xxx" target = resolve_configured_personal_recipient() assert target.ok assert target.identifier_type == "user_id" def test_recipient_missing(settings): settings.FEISHU_DEFAULT_USER_OPEN_ID = "" settings.FEISHU_DEFAULT_USER_ID = "" target = resolve_configured_personal_recipient() assert not target.ok assert target.error_code == "recipient_missing" def test_build_feishu_post_message_contains_summary(settings): settings.PUBLIC_BASE_URL = "http://example.test" settings.FEISHU_DEFAULT_USER_OPEN_ID = "ou_xxx" target = resolve_configured_personal_recipient() context = NotificationContext( workflow_type="file_summary", workflow_name="自动汇总", workflow_batch_id=1, workflow_batch_no="FS-001", workflow_status="success", trigger_user_id=1, trigger_username="owner", title="自动汇总完成", summary_lines=("文件 3 个", "异常 0 个"), next_step="查看汇总结果", result_path="/summary/1/", ) payload = build_feishu_post_message(context, target) assert payload["receive_id"] == "ou_xxx" content = json.loads(payload["content"]) assert content["zh_cn"]["title"] == "自动汇总完成" assert "http://example.test/summary/1/" in payload["content"] def test_send_personal_message_success(monkeypatch, settings): settings.FEISHU_MESSAGE_API_URL = "http://feishu/messages" requests = [] def fake_post(*args, **kwargs): requests.append(kwargs) return FakeResponse({"code": 0, "data": {"message_id": "om_xxx"}}) monkeypatch.setattr("review_agent.notifications.feishu_message_api.httpx.post", fake_post) result = send_personal_message( tenant_access_token="token", receive_id_type="open_id", payload={"receive_id": "ou_xxx"}, ) assert result.ok assert result.external_message_id == "om_xxx" assert requests[0]["headers"]["Authorization"] == "Bearer token" def test_send_personal_message_api_error(monkeypatch, settings): settings.FEISHU_MESSAGE_API_URL = "http://feishu/messages" monkeypatch.setattr( "review_agent.notifications.feishu_message_api.httpx.post", lambda *args, **kwargs: FakeResponse({"code": 230001, "msg": "bad receive_id"}), ) result = send_personal_message( tenant_access_token="token", receive_id_type="open_id", payload={"receive_id": "bad"}, ) assert not result.ok assert result.error_code == "230001" def test_send_personal_message_refreshes_token_once(monkeypatch, settings): settings.FEISHU_MESSAGE_API_URL = "http://feishu/messages" settings.FEISHU_APP_ID = "cli_a" settings.FEISHU_APP_SECRET = "secret" calls = {"message": 0} def fake_message_post(*args, **kwargs): calls["message"] += 1 if calls["message"] == 1: return FakeResponse({"code": 99991663, "msg": "token expired"}) return FakeResponse({"code": 0, "data": {"message_id": "om_retry"}}) monkeypatch.setattr("review_agent.notifications.feishu_message_api.httpx.post", fake_message_post) monkeypatch.setattr( "review_agent.notifications.feishu_token.httpx.post", lambda *args, **kwargs: FakeResponse({"code": 0, "tenant_access_token": "fresh", "expire": 7200}), ) result = send_personal_message( tenant_access_token="stale", receive_id_type="open_id", payload={"receive_id": "ou_xxx"}, ) assert result.ok assert result.refreshed_token assert calls["message"] == 2