ForcePilot/backend/test/unit/channels/adapters/googlechat/test_googlechat.py
Kris 1242999888 test: 添加多个渠道适配器的单元测试用例
本次提交新增了超过20个单元测试文件,覆盖Twitch、Telegram、WeChat、Discord、Slack、Mattermost等多个渠道适配器以及核心组件的测试:
1.  新增Twitch令牌工具、限流器、去重器相关测试
2.  新增Telegram分块、格式、安全策略、流媒体相关测试
3.  新增微信适配器、限流、会话、链接格式、监控等测试
4.  新增Discord命令、会话、格式化、Webhook相关测试
5.  新增Slack、Mattermost、Nostr、Signal、Zalo、NextcloudTalk等渠道的测试用例
6.  新增核心路由、会话路由、文本分块器的冒烟测试
2026-05-13 18:03:43 +08:00

2141 lines
78 KiB
Python

from __future__ import annotations
import asyncio
import json
import time
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.googlechat.approval_auth import (
build_exec_approval_context,
is_fully_approved,
normalize_approver_id,
record_approval_decision,
resolve_approver_ids,
)
from yuxi.channels.adapters.googlechat.auth import (
GoogleChatCertCache,
get_cert_cache,
verify_project_number_token,
)
from yuxi.channels.adapters.googlechat.cards import (
build_approval_card,
build_exec_approval_card,
build_form_card,
build_info_card,
build_poll_card,
build_selection_card,
)
from yuxi.channels.adapters.googlechat.directory import list_groups, list_peers, normalize_id
from yuxi.channels.adapters.googlechat.doctor import run_diagnostics
from yuxi.channels.adapters.googlechat.formatter import (
chunk_text_for_outbound,
format_outbound,
resolve_reply_to_mode,
sanitize_text,
)
from yuxi.channels.adapters.googlechat.mentions import parse_mentions
from yuxi.channels.adapters.googlechat.msg_cache import (
SentMessageCache,
track_sent_message,
update_sent_message_status,
)
from yuxi.channels.adapters.googlechat.normalizer import (
detect_message_edit,
extract_attachments,
is_bot_message,
is_forwarded_message,
map_event_type,
map_msg_type,
normalize_inbound,
resolve_route_envelope,
)
from yuxi.channels.adapters.googlechat.policy import (
DmPolicy,
GoogleChatPolicy,
GroupPolicy,
)
from yuxi.channels.adapters.googlechat.proxy import (
build_google_auth_request,
build_proxies_dict,
resolve_proxy_config,
resolve_tls_config,
)
from yuxi.channels.adapters.googlechat.secret_contract import (
get_required_secrets,
get_secret_contracts,
get_sensitive_fields,
is_sensitive_field,
register_secret_contract,
)
from yuxi.channels.adapters.googlechat.session import build_thread_id
from yuxi.channels.adapters.googlechat.setup import (
SetupWizard,
get_setup_guide,
wizard_validate_service_account,
)
from yuxi.channels.adapters.googlechat.slash_commands import extract_command, get_command_help
from yuxi.channels.adapters.googlechat.ssrf import (
apply_ssrf_guard,
sanitize_google_auth_init,
sanitize_request_kwargs,
)
from yuxi.channels.adapters.googlechat.streaming import StreamManager
from yuxi.channels.adapters.googlechat.target import (
detect_deprecated_target,
is_googlechat_space_target,
is_googlechat_user_target,
normalize_googlechat_target,
resolve_targets,
strip_message_suffix,
)
from yuxi.channels.adapters.googlechat.threads import (
extract_thread_metadata,
is_thread_message,
parse_thread_key,
)
from yuxi.channels.models import (
Attachment,
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChannelType,
ChatType,
EventType,
MessageType,
MentionsInfo,
)
# ============================================================================
# normalizer.py tests
# ============================================================================
class TestMapEventType:
def test_message_event(self):
assert map_event_type("MESSAGE") == EventType.MESSAGE_RECEIVED
def test_message_update_event(self):
assert map_event_type("MESSAGE_UPDATE") == EventType.MESSAGE_UPDATED
def test_message_delete_event(self):
assert map_event_type("MESSAGE_DELETE") == EventType.MESSAGE_DELETED
def test_added_to_space_event(self):
assert map_event_type("ADDED_TO_SPACE") == EventType.BOT_ADDED
def test_removed_from_space_event(self):
assert map_event_type("REMOVED_FROM_SPACE") == EventType.BOT_REMOVED
def test_card_clicked_event(self):
assert map_event_type("CARD_CLICKED") == EventType.CARD_ACTION
def test_unknown_event_defaults_to_message_received(self):
assert map_event_type("UNKNOWN_EVENT") == EventType.MESSAGE_RECEIVED
def test_empty_event_string(self):
assert map_event_type("") == EventType.MESSAGE_RECEIVED
class TestIsBotMessage:
def test_bot_sender(self):
assert is_bot_message({"sender": {"type": "BOT"}}) is True
def test_human_sender(self):
assert is_bot_message({"sender": {"type": "HUMAN"}}) is False
def test_no_sender_field(self):
assert is_bot_message({}) is False
def test_sender_without_type(self):
assert is_bot_message({"sender": {"name": "test"}}) is False
class TestIsForwardedMessage:
def test_has_retention_settings(self):
assert is_forwarded_message({"retentionSettings": {"state": "PERMANENT"}}) is True
def test_no_retention_no_times(self):
assert is_forwarded_message({}) is False
def test_empty_dict(self):
assert is_forwarded_message({}) is False
class TestDetectMessageEdit:
def test_different_create_and_update_times(self):
assert detect_message_edit({"createTime": "2024-01-01T00:00:00Z", "lastUpdateTime": "2024-01-02T00:00:00Z"}) is True
def test_same_times_not_edited(self):
assert detect_message_edit({"createTime": "2024-01-01T00:00:00Z", "lastUpdateTime": "2024-01-01T00:00:00Z"}) is False
def test_no_times(self):
assert detect_message_edit({}) is False
def test_only_create_time(self):
assert detect_message_edit({"createTime": "2024-01-01T00:00:00Z"}) is False
def test_only_update_time(self):
assert detect_message_edit({"lastUpdateTime": "2024-01-01T00:00:00Z"}) is False
class TestResolveRouteEnvelope:
def test_direct_chat(self):
result = resolve_route_envelope(ChatType.DIRECT, "spaces/dm-123", "users/user@example.com")
assert result["peer_kind"] == "direct"
assert result["peer_id"] == "users/user@example.com"
assert result["peer_key"] == "direct:user@example.com"
route = result["route"]
assert route["content"]["type"] == "direct"
def test_direct_chat_user_with_users_prefix(self):
result = resolve_route_envelope(ChatType.DIRECT, "spaces/dm-123", "users/user@example.com")
assert result["peer_key"] == "direct:user@example.com"
def test_direct_chat_user_without_prefix(self):
result = resolve_route_envelope(ChatType.DIRECT, "spaces/dm-123", "user@example.com")
assert result["peer_key"] == "direct:user@example.com"
def test_group_chat(self):
result = resolve_route_envelope(ChatType.SPACE, "spaces/ABC123", "")
assert result["peer_kind"] == "group"
assert result["peer_id"] == "spaces/ABC123"
assert result["peer_key"] == "space:ABC123"
route = result["route"]
assert route["content"]["type"] == "space"
def test_group_chat_space_without_prefix(self):
result = resolve_route_envelope(ChatType.SPACE, "ABC123", "")
assert result["peer_id"] == "ABC123"
assert result["peer_key"] == "space:ABC123"
class TestExtractAttachments:
def test_image_attachment(self):
msg = {"attachment": [{"name": "img1", "contentName": "photo.png", "contentType": "image/png"}]}
atts = extract_attachments(msg)
assert len(atts) == 1
assert atts[0].type == "image"
assert atts[0].mime_type == "image/png"
assert atts[0].filename == "photo.png"
def test_video_attachment(self):
msg = {"attachment": [{"name": "video1", "contentName": "clip.mp4", "contentType": "video/mp4"}]}
atts = extract_attachments(msg)
assert len(atts) == 1
assert atts[0].type == "video"
assert atts[0].mime_type == "video/mp4"
def test_audio_attachment(self):
msg = {"attachment": [{"name": "audio1", "contentName": "note.mp3", "contentType": "audio/mp3"}]}
atts = extract_attachments(msg)
assert len(atts) == 1
assert atts[0].type == "audio"
def test_file_attachment(self):
msg = {"attachment": [{"name": "file1", "contentName": "doc.pdf", "contentType": "application/pdf"}]}
atts = extract_attachments(msg)
assert len(atts) == 1
assert atts[0].type == "file"
def test_attachments_key_variant(self):
msg = {"attachments": [{"name": "file2", "contentName": "data.csv", "contentType": "text/csv"}]}
atts = extract_attachments(msg)
assert len(atts) == 1
assert atts[0].type == "file"
def test_no_attachments(self):
msg = {}
atts = extract_attachments(msg)
assert atts == []
def test_empty_attachments_list(self):
msg = {"attachment": []}
atts = extract_attachments(msg)
assert atts == []
def test_multiple_mixed_attachments(self):
msg = {
"attachment": [
{"name": "img", "contentName": "a.png", "contentType": "image/png"},
{"name": "doc", "contentName": "b.pdf", "contentType": "application/pdf"},
{"name": "vid", "contentName": "c.mp4", "contentType": "video/mp4"},
]
}
atts = extract_attachments(msg)
assert len(atts) == 3
assert atts[0].type == "image"
assert atts[1].type == "file"
assert atts[2].type == "video"
def test_file_id_encoded(self):
msg = {"attachment": [{"name": "spaces/ABC/messages/xyz/attachments/foo", "contentType": "image/png"}]}
atts = extract_attachments(msg)
assert len(atts) == 1
decoded = json.loads(atts[0].file_id)
assert decoded["name"] == "spaces/ABC/messages/xyz/attachments/foo"
class TestMapMsgType:
def test_card_action_event(self):
assert map_msg_type({}, EventType.CARD_ACTION) == MessageType.CARD
def test_image_content_type(self):
assert map_msg_type({"attachment": [{"contentType": "image/png"}]}, EventType.MESSAGE_RECEIVED) == MessageType.IMAGE
def test_video_content_type(self):
assert map_msg_type({"attachment": [{"contentType": "video/mp4"}]}, EventType.MESSAGE_RECEIVED) == MessageType.VIDEO
def test_audio_content_type(self):
assert map_msg_type({"attachment": [{"contentType": "audio/mp3"}]}, EventType.MESSAGE_RECEIVED) == MessageType.AUDIO
def test_file_content_type(self):
assert map_msg_type({"attachment": [{"contentType": "application/pdf"}]}, EventType.MESSAGE_RECEIVED) == MessageType.FILE
def test_cards_v2_message(self):
assert map_msg_type({"cardsV2": [{}], "text": ""}, EventType.MESSAGE_RECEIVED) == MessageType.CARD
def test_cards_v2_snake_case(self):
assert map_msg_type({"cards_v2": [{}], "text": ""}, EventType.MESSAGE_RECEIVED) == MessageType.CARD
def test_text_message(self):
assert map_msg_type({"text": "hello"}, EventType.MESSAGE_RECEIVED) == MessageType.TEXT
def test_empty_message_defaults_to_text(self):
assert map_msg_type({}, EventType.MESSAGE_RECEIVED) == MessageType.TEXT
class TestNormalizeInbound:
def _sample_payload(self, **overrides):
payload = {
"event": {
"type": "MESSAGE",
"eventTime": "2024-01-01T00:00:00Z",
"message": {
"name": "spaces/ABC/messages/xyz",
"sender": {"name": "users/user1@example.com", "type": "HUMAN"},
"text": "Hello world",
"thread": {"threadKey": ""},
},
"space": {"name": "spaces/ABC", "spaceType": "SPACE"},
"user": {"name": "users/user1@example.com"},
},
}
for key, value in overrides.items():
if key in ("message", "space", "user"):
payload["event"][key].update(value)
elif key == "event_type":
payload["event"]["type"] = value
else:
payload[key] = value
return payload
def test_dm_message(self):
payload = self._sample_payload(
space={"name": "spaces/DM123", "spaceType": "DIRECT_MESSAGE"}
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.identity.channel_id == "test-ch"
assert result.identity.channel_type == ChannelType.GOOGLE_CHAT
assert result.identity.channel_user_id == "users/user1@example.com"
assert result.identity.channel_chat_id == "spaces/DM123"
assert result.identity.channel_message_id == "spaces/ABC/messages/xyz"
assert result.chat_type == ChatType.DIRECT
assert result.event_type == EventType.MESSAGE_RECEIVED
assert result.content == "Hello world"
assert result.message_type == MessageType.TEXT
def test_space_message(self):
payload = self._sample_payload()
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.chat_type == ChatType.SPACE
def test_thread_message(self):
payload = self._sample_payload(
message={"thread": {"threadKey": "thread-001"}}
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.chat_type == ChatType.THREAD
assert result.metadata["thread_key"] == "thread-001"
def test_card_action_event(self):
payload = self._sample_payload(
event_type="CARD_CLICKED",
action={"actionMethodName": "test_action"},
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.event_type == EventType.CARD_ACTION
assert result.message_type == MessageType.CARD
def test_added_to_space(self):
payload = self._sample_payload(event_type="ADDED_TO_SPACE")
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.event_type == EventType.BOT_ADDED
def test_removed_from_space(self):
payload = self._sample_payload(event_type="REMOVED_FROM_SPACE")
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.event_type == EventType.BOT_REMOVED
def test_message_update(self):
payload = self._sample_payload(event_type="MESSAGE_UPDATE")
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.event_type == EventType.MESSAGE_UPDATED
def test_slash_command(self):
payload = self._sample_payload(
message={"text": "/help"},
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.message_type == MessageType.COMMAND
assert result.metadata["slash_command"] == "/help"
def test_slash_command_with_args(self):
payload = self._sample_payload(
message={"text": "/reset confirm"},
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.metadata["slash_command"] == "/reset"
assert result.metadata["slash_args"] == "confirm"
def test_unknown_slash_command_treated_as_text(self):
payload = self._sample_payload(
message={"text": "/unknown_cmd"},
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.message_type == MessageType.TEXT
assert result.metadata["slash_command"] is None
def test_empty_content(self):
payload = self._sample_payload(
message={"text": ""},
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.content == ""
def test_metadata_contains_space_info(self):
payload = self._sample_payload()
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.metadata["space_name"] == "spaces/ABC"
assert result.metadata["space_type"] == "SPACE"
assert result.metadata["sender_name"] == "users/user1@example.com"
assert result.metadata["sender_type"] == "HUMAN"
def test_edited_message(self):
payload = self._sample_payload(
message={"createTime": "2024-01-01T00:00:00Z", "lastUpdateTime": "2024-01-02T00:00:00Z"}
)
result = normalize_inbound("test-ch", ChannelType.GOOGLE_CHAT, payload)
assert result.metadata["is_edited"] is True
# ============================================================================
# mentions.py tests
# ============================================================================
class TestParseMentions:
def test_no_annotations(self):
result = parse_mentions({})
assert result.mentioned_user_ids == []
assert result.is_bot_mentioned is False
def test_user_mention(self):
msg = {
"annotations": [
{"type": "USER_MENTION", "userMention": {"user": {"name": "users/user1"}, "type": "HUMAN"}}
]
}
result = parse_mentions(msg)
assert "users/user1" in result.mentioned_user_ids
assert result.is_bot_mentioned is False
def test_bot_mention_by_type(self):
msg = {
"annotations": [
{"type": "USER_MENTION", "userMention": {"type": "BOT"}}
]
}
result = parse_mentions(msg)
assert result.is_bot_mentioned is True
def test_bot_mention_by_name(self):
msg = {
"annotations": [
{"type": "USER_MENTION", "userMention": {"user": {"name": "users/bot@example.com"}, "type": "HUMAN"}}
]
}
result = parse_mentions(msg, bot_user="users/bot@example.com")
assert result.is_bot_mentioned is True
def test_multiple_mentions(self):
msg = {
"annotations": [
{"type": "USER_MENTION", "userMention": {"user": {"name": "users/user1"}}},
{"type": "USER_MENTION", "userMention": {"user": {"name": "users/user2"}}},
{"type": "SLASH_COMMAND", "slashCommand": {}},
]
}
result = parse_mentions(msg)
assert len(result.mentioned_user_ids) == 2
assert "users/user1" in result.mentioned_user_ids
assert "users/user2" in result.mentioned_user_ids
def test_empty_annotations_list(self):
result = parse_mentions({"annotations": []})
assert result.mentioned_user_ids == []
def test_non_user_mention_annotation(self):
msg = {
"annotations": [
{"type": "SLASH_COMMAND", "slashCommand": {}},
]
}
result = parse_mentions(msg)
assert result.mentioned_user_ids == []
assert result.is_bot_mentioned is False
# ============================================================================
# formatter.py tests
# ============================================================================
class TestSanitizeText:
def test_null_bytes_removed(self):
assert sanitize_text("hel\x00lo") == "hello"
def test_control_chars_removed(self):
assert sanitize_text("hello\x01\x08\x0b\x0c\x0e") == "hello"
def test_trailing_whitespace_stripped(self):
assert sanitize_text(" hello \n ") == " hello"
def test_normal_text_unchanged(self):
assert sanitize_text("Hello World 你好") == "Hello World 你好"
def test_empty_string(self):
assert sanitize_text("") == ""
def test_only_control_chars(self):
assert sanitize_text("\x00\x01\x02") == ""
class TestChunkTextForOutbound:
def test_empty_text(self):
assert chunk_text_for_outbound("") == []
def test_short_text(self):
result = chunk_text_for_outbound("Hello", 4000)
assert result == ["Hello"]
def test_text_at_limit(self):
text = "x" * 4000
result = chunk_text_for_outbound(text, 4000)
assert len(result) == 1
assert result[0] == text
def test_text_over_limit_chunked(self):
text = "A" * 8000
result = chunk_text_for_outbound(text, 2000)
assert len(result) >= 4
total = sum(len(c) for c in result)
assert total >= len(text) - 100
def test_long_text_with_newlines(self):
text = "Line 1\n\nLine 2\nLine 3" * 1000
result = chunk_text_for_outbound(text, 4000)
assert len(result) >= 1
for chunk in result:
assert len(chunk) <= 4000
def test_all_chunks_within_limit(self):
text = "B" * 10000
result = chunk_text_for_outbound(text, 3000)
for chunk in result:
assert len(chunk) <= 3000
def test_chunk_with_custom_limit(self):
text = "C" * 500
result = chunk_text_for_outbound(text, 200)
assert len(result) >= 3
for chunk in result:
assert len(chunk) <= 200
def test_no_empty_chunks(self):
text = "D" * 500
result = chunk_text_for_outbound(text, 200)
for chunk in result:
assert len(chunk) > 0
def test_code_block_preserved(self):
text = "Before\n```\ncode block here\n```\nAfter" * 100
result = chunk_text_for_outbound(text, 4000)
assert len(result) >= 1
def test_whitespace_only_treated_as_empty(self):
assert chunk_text_for_outbound(" ") == [" "]
class TestFormatOutbound:
def _make_response(self, **overrides):
defaults = {
"identity": ChannelIdentity(
channel_id="gchat",
channel_type=ChannelType.GOOGLE_CHAT,
channel_user_id="users/user1",
channel_chat_id="spaces/ABC",
),
"content": "Hello",
}
defaults.update(overrides)
return ChannelResponse(**defaults)
def test_text_output(self):
payload = format_outbound(self._make_response(content="Hello World"))
assert payload["text"] == "Hello World"
def test_empty_content(self):
payload = format_outbound(self._make_response(content=""))
assert payload["text"] in (" ", "")
def test_image_attachment(self):
resp = self._make_response(
attachments=[Attachment(type="image", url="https://example.com/img.png", filename="photo.png")]
)
payload = format_outbound(resp)
assert "cards_v2" in payload
assert len(payload["cards_v2"]) == 1
widget = payload["cards_v2"][0]["card"]["sections"][0]["widgets"][0]
assert widget["image"]["imageUrl"] == "https://example.com/img.png"
assert widget["image"]["altText"] == "photo.png"
def test_video_attachment(self):
resp = self._make_response(
attachments=[Attachment(type="video", url="https://example.com/vid.mp4")]
)
payload = format_outbound(resp)
assert "cards_v2" in payload
widget = payload["cards_v2"][0]["card"]["sections"][0]["widgets"][0]
assert widget["image"]["imageUrl"] == "https://example.com/vid.mp4"
def test_thread_key(self):
resp = self._make_response(metadata={"thread_key": "thread-001"})
payload = format_outbound(resp)
assert payload["messageReplyOption"] == "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"
assert payload["thread"]["threadKey"] == "thread-001"
def test_silent_notification(self):
resp = self._make_response(metadata={"silent": True})
payload = format_outbound(resp)
assert payload["disableNotification"] is True
def test_prebuilt_card(self):
resp = self._make_response(
metadata={"card": {"cards_v2": [{"card_id": "card1", "card": {}}]}}
)
payload = format_outbound(resp)
assert "cards_v2" in payload
assert payload["cards_v2"][0]["card_id"] == "card1"
def test_approval_card_type(self):
resp = self._make_response(
content="Approve this",
metadata={"card_type": "approval", "card_params": {"title": "Approval Request"}},
)
payload = format_outbound(resp)
assert "cards_v2" in payload
def test_poll_card_type(self):
resp = self._make_response(
content="Poll question",
metadata={
"card_type": "poll",
"card_params": {"options": ["Yes", "No"]},
},
)
payload = format_outbound(resp)
assert "cards_v2" in payload
def test_info_card_type(self):
resp = self._make_response(
metadata={
"card_type": "info",
"card_params": {"fields": {"Key": "Value"}},
},
)
payload = format_outbound(resp)
assert "cards_v2" in payload
def test_form_card_type(self):
resp = self._make_response(
metadata={
"card_type": "form",
"card_params": {"fields": [{"name": "email", "label": "Email"}]},
},
)
payload = format_outbound(resp)
assert "cards_v2" in payload
def test_selection_card_type(self):
resp = self._make_response(
metadata={
"card_type": "selection",
"card_params": {"options": [{"text": "Opt A", "value": "a"}]},
},
)
payload = format_outbound(resp)
assert "cards_v2" in payload
def test_exec_approval_card_type(self):
resp = self._make_response(
metadata={
"card_type": "exec_approval",
"card_params": {"title": "Execute?"},
},
)
payload = format_outbound(resp)
assert "cards_v2" in payload
def test_long_content_truncated(self):
long_text = "A" * 5000
resp = self._make_response(content=long_text)
payload = format_outbound(resp)
assert len(payload["text"]) <= 4003
assert payload["text"].endswith("...")
def test_no_metadata_no_extras(self):
payload = format_outbound(self._make_response())
assert "cards_v2" not in payload
assert "thread" not in payload
class TestResolveReplyToMode:
def test_empty_config(self):
assert resolve_reply_to_mode(None) == "off"
assert resolve_reply_to_mode({}) == "off"
def test_valid_modes(self):
assert resolve_reply_to_mode({"replyToMode": "first"}) == "first"
assert resolve_reply_to_mode({"replyToMode": "all"}) == "all"
assert resolve_reply_to_mode({"replyToMode": "off"}) == "off"
def test_snake_case_key(self):
assert resolve_reply_to_mode({"reply_to_mode": "first"}) == "first"
def test_invalid_mode_defaults(self):
assert resolve_reply_to_mode({"replyToMode": "invalid"}) == "off"
def test_case_insensitive(self):
assert resolve_reply_to_mode({"replyToMode": "FIRST"}) == "first"
def test_whitespace_trimmed(self):
assert resolve_reply_to_mode({"replyToMode": " all "}) == "all"
# ============================================================================
# policy.py tests
# ============================================================================
class TestDmPolicy:
def test_enum_values(self):
assert DmPolicy.OPEN == "open"
assert DmPolicy.PAIRING == "pairing"
assert DmPolicy.ALLOWLIST == "allowlist"
assert DmPolicy.DISABLED == "disabled"
class TestGroupPolicy:
def test_enum_values(self):
assert GroupPolicy.OPEN == "open"
assert GroupPolicy.ALLOWLIST == "allowlist"
assert GroupPolicy.DISABLED == "disabled"
class TestGoogleChatPolicy:
def test_from_config_empty(self):
policy = GoogleChatPolicy.from_config(None)
assert policy.dm_policy == DmPolicy.OPEN
assert policy.group_policy == GroupPolicy.OPEN
assert policy.allow_from == []
assert policy.require_mention is False
def test_from_config_with_values(self):
policy = GoogleChatPolicy.from_config({
"dmPolicy": "allowlist",
"groupPolicy": "disabled",
"requireMention": True,
"allowFrom": ["users/a@example.com"],
})
assert policy.dm_policy == DmPolicy.ALLOWLIST
assert policy.group_policy == GroupPolicy.DISABLED
assert policy.require_mention is True
assert "users/a@example.com" in policy.allow_from
def test_from_config_snake_case_keys(self):
policy = GoogleChatPolicy.from_config({
"dm_policy": "pairing",
"group_policy": "allowlist",
"allow_from": ["users/b@example.com"],
"group_allow_from": ["spaces/ABC"],
})
assert policy.dm_policy == DmPolicy.PAIRING
assert policy.group_policy == GroupPolicy.ALLOWLIST
assert "users/b@example.com" in policy.allow_from
assert "spaces/ABC" in policy.group_allow_from
def test_from_config_comma_separated_allow_from(self):
policy = GoogleChatPolicy.from_config({
"allowFrom": "users/a@x.com, users/b@x.com",
})
assert len(policy.allow_from) == 2
def test_from_config_invalid_dm_policy_defaults_to_open(self):
policy = GoogleChatPolicy.from_config({"dmPolicy": "invalid"})
assert policy.dm_policy == DmPolicy.OPEN
def test_from_config_invalid_group_policy_defaults_to_open(self):
policy = GoogleChatPolicy.from_config({"groupPolicy": "invalid"})
assert policy.group_policy == GroupPolicy.OPEN
def test_check_dm_access_open(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.OPEN)
assert policy.check_dm_access("any_user") is True
def test_check_dm_access_disabled(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.DISABLED)
assert policy.check_dm_access("any_user") is False
def test_check_dm_access_pairing(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.PAIRING)
assert policy.check_dm_access("any_user") is True
def test_check_dm_access_allowlist_match(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.ALLOWLIST, allow_from=["users/a@x.com"])
assert policy.check_dm_access("users/a@x.com") is True
def test_check_dm_access_allowlist_no_match(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.ALLOWLIST, allow_from=["users/a@x.com"])
assert policy.check_dm_access("users/b@x.com") is False
def test_check_dm_access_allowlist_wildcard(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.ALLOWLIST, allow_from=["*"])
assert policy.check_dm_access("any_user") is True
def test_check_dm_access_empty_allowlist(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.ALLOWLIST, allow_from=[])
assert policy.check_dm_access("any_user") is False
def test_check_group_access_open(self):
policy = GoogleChatPolicy(group_policy=GroupPolicy.OPEN)
assert policy.check_group_access("spaces/ABC") is True
def test_check_group_access_disabled(self):
policy = GoogleChatPolicy(group_policy=GroupPolicy.DISABLED)
assert policy.check_group_access("spaces/ABC") is False
def test_check_group_access_allowlist_match(self):
policy = GoogleChatPolicy(group_policy=GroupPolicy.ALLOWLIST, group_allow_from=["spaces/ABC"])
assert policy.check_group_access("spaces/ABC") is True
def test_check_group_access_allowlist_no_match(self):
policy = GoogleChatPolicy(group_policy=GroupPolicy.ALLOWLIST, group_allow_from=["spaces/XYZ"])
assert policy.check_group_access("spaces/ABC") is False
def test_check_group_per_group_disabled(self):
policy = GoogleChatPolicy(
group_policy=GroupPolicy.OPEN,
groups={"spaces/ABC": MagicMock(enabled=False)},
)
policy.groups["spaces/ABC"].enabled = False
assert policy.check_group_access("spaces/ABC") is False
def test_check_inbound_direct_open(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.OPEN)
msg = ChannelMessage(
identity=ChannelIdentity(channel_id="gc", channel_type=ChannelType.GOOGLE_CHAT, channel_user_id="users/u1", channel_chat_id="spaces/dm"),
chat_type=ChatType.DIRECT,
content="hi",
)
assert policy.check_inbound(msg) is True
def test_check_inbound_direct_disabled(self):
policy = GoogleChatPolicy(dm_policy=DmPolicy.DISABLED)
msg = ChannelMessage(
identity=ChannelIdentity(channel_id="gc", channel_type=ChannelType.GOOGLE_CHAT, channel_user_id="users/u1", channel_chat_id="spaces/dm"),
chat_type=ChatType.DIRECT,
content="hi",
)
assert policy.check_inbound(msg) is False
def test_check_inbound_group_require_mention_not_mentioned(self):
policy = GoogleChatPolicy(require_mention=True)
msg = ChannelMessage(
identity=ChannelIdentity(channel_id="gc", channel_type=ChannelType.GOOGLE_CHAT, channel_user_id="users/u1", channel_chat_id="spaces/ABC"),
chat_type=ChatType.SPACE,
content="Hello",
mentions=MentionsInfo(mentioned_user_ids=[], is_bot_mentioned=False),
metadata={"space_name": "spaces/ABC"},
)
assert policy.check_inbound(msg) is False
def test_check_inbound_group_require_mention_mentioned(self):
policy = GoogleChatPolicy(require_mention=True)
msg = ChannelMessage(
identity=ChannelIdentity(channel_id="gc", channel_type=ChannelType.GOOGLE_CHAT, channel_user_id="users/u1", channel_chat_id="spaces/ABC"),
chat_type=ChatType.SPACE,
content="Hello @bot",
mentions=MentionsInfo(mentioned_user_ids=["users/bot"], is_bot_mentioned=True),
metadata={"space_name": "spaces/ABC"},
)
assert policy.check_inbound(msg) is True
def test_check_inbound_direct_ignores_require_mention(self):
policy = GoogleChatPolicy(require_mention=True, dm_policy=DmPolicy.OPEN)
msg = ChannelMessage(
identity=ChannelIdentity(channel_id="gc", channel_type=ChannelType.GOOGLE_CHAT, channel_user_id="users/u1", channel_chat_id="spaces/dm"),
chat_type=ChatType.DIRECT,
content="Hello",
mentions=MentionsInfo(mentioned_user_ids=[], is_bot_mentioned=False),
)
assert policy.check_inbound(msg) is True
def test_per_group_users_whitelist(self):
from yuxi.channels.adapters.googlechat.policy import PerGroupConfig
policy = GoogleChatPolicy(
group_policy=GroupPolicy.OPEN,
groups={"spaces/ABC": PerGroupConfig(users=["users/allowed_user"])},
)
msg = ChannelMessage(
identity=ChannelIdentity(channel_id="gc", channel_type=ChannelType.GOOGLE_CHAT, channel_user_id="users/allowed_user", channel_chat_id="spaces/ABC"),
chat_type=ChatType.SPACE,
content="Hello",
metadata={"space_name": "spaces/ABC"},
)
assert policy.check_inbound(msg) is True
def test_per_group_users_whitelist_blocked(self):
from yuxi.channels.adapters.googlechat.policy import PerGroupConfig
policy = GoogleChatPolicy(
group_policy=GroupPolicy.OPEN,
groups={"spaces/ABC": PerGroupConfig(users=["users/allowed_user"])},
)
msg = ChannelMessage(
identity=ChannelIdentity(channel_id="gc", channel_type=ChannelType.GOOGLE_CHAT, channel_user_id="users/blocked_user", channel_chat_id="spaces/ABC"),
chat_type=ChatType.SPACE,
content="Hello",
metadata={"space_name": "spaces/ABC"},
)
assert policy.check_inbound(msg) is False
def test_get_per_group_system_prompt(self):
from yuxi.channels.adapters.googlechat.policy import PerGroupConfig
policy = GoogleChatPolicy(
groups={"spaces/ABC": PerGroupConfig(system_prompt="Custom prompt")},
)
assert policy.get_per_group_system_prompt("spaces/ABC") == "Custom prompt"
assert policy.get_per_group_system_prompt("spaces/XYZ") == ""
# ============================================================================
# slash_commands.py tests
# ============================================================================
class TestExtractCommand:
def test_help_command(self):
cmd, args = extract_command("/help")
assert cmd == "/help"
assert args == ""
def test_reset_command_with_args(self):
cmd, args = extract_command("/reset confirm")
assert cmd == "/reset"
assert args == "confirm"
def test_status_command(self):
cmd, args = extract_command("/status")
assert cmd == "/status"
def test_history_command(self):
cmd, args = extract_command("/history")
assert cmd == "/history"
def test_context_command(self):
cmd, args = extract_command("/context")
assert cmd == "/context"
def test_summary_command(self):
cmd, args = extract_command("/summary")
assert cmd == "/summary"
def test_not_a_command(self):
cmd, args = extract_command("Hello world")
assert cmd is None
assert args == "Hello world"
def test_unknown_command(self):
cmd, args = extract_command("/unknown_cmd with args")
assert cmd is None
assert args == "/unknown_cmd with args"
def test_empty_string(self):
cmd, args = extract_command("")
assert cmd is None
assert args == ""
def test_case_insensitive(self):
cmd, args = extract_command("/HELP")
assert cmd == "/help"
def test_command_with_trailing_whitespace(self):
cmd, args = extract_command(" /help ")
assert cmd == "/help"
class TestGetCommandHelp:
def test_returns_non_empty_string(self):
help_text = get_command_help()
assert len(help_text) > 0
assert "可用命令" in help_text
def test_includes_help_command(self):
help_text = get_command_help()
assert "/help" in help_text
def test_includes_reset_command(self):
help_text = get_command_help()
assert "/reset" in help_text
def test_includes_status_command(self):
help_text = get_command_help()
assert "/status" in help_text
# ============================================================================
# target.py tests
# ============================================================================
class TestStripMessageSuffix:
def test_strips_messages_suffix(self):
result, was_stripped = strip_message_suffix("spaces/ABC/messages/xyz123")
assert result == "spaces/ABC"
assert was_stripped is True
def test_no_strip_when_no_suffix(self):
result, was_stripped = strip_message_suffix("spaces/ABC")
assert result == "spaces/ABC"
assert was_stripped is False
def test_empty_string(self):
result, was_stripped = strip_message_suffix("")
assert result == ""
assert was_stripped is False
class TestDetectDeprecatedTarget:
def test_valid_spaces_prefix(self):
assert detect_deprecated_target("spaces/ABC123") is None
def test_valid_users_prefix(self):
assert detect_deprecated_target("users/user@example.com") is None
def test_deprecated_email_target(self):
result = detect_deprecated_target("user@example.com")
assert result is not None
assert "user@example.com" in result
def test_with_channel_prefix_spaces(self):
assert detect_deprecated_target("googlechat:spaces/ABC") is None
def test_with_channel_prefix_users(self):
assert detect_deprecated_target("gchat:users/user@x.com") is None
class TestNormalizeGooglechatTarget:
def test_empty_target(self):
result = normalize_googlechat_target("")
assert result["type"] == "unknown"
def test_space_target(self):
result = normalize_googlechat_target("spaces/ABC123")
assert result["type"] == "space"
assert result["space_id"] == "ABC123"
assert result["space_name"] == "spaces/ABC123"
def test_thread_target(self):
result = normalize_googlechat_target("spaces/ABC123/threads/thread001")
assert result["type"] == "thread"
assert result["space_id"] == "ABC123"
assert result["thread_key"] == "thread001"
def test_user_target(self):
result = normalize_googlechat_target("users/user@example.com")
assert result["type"] == "user"
assert result["user_id"] == "user@example.com"
assert result["user_name"] == "users/user@example.com"
def test_user_colon_prefix(self):
result = normalize_googlechat_target("user:user@example.com")
assert result["type"] == "user"
assert result["user_id"] == "user@example.com"
def test_email_conversion(self):
result = normalize_googlechat_target("user@example.com")
assert result["type"] == "user"
assert result["user_id"] == "user@example.com"
def test_channel_prefix_stripped(self):
result = normalize_googlechat_target("googlechat:spaces/ABC")
assert result["type"] == "space"
def test_gchat_prefix_stripped(self):
result = normalize_googlechat_target("gchat:users/user@x.com")
assert result["type"] == "user"
def test_google_chat_prefix_stripped(self):
result = normalize_googlechat_target("google-chat:spaces/XYZ")
assert result["type"] == "space"
def test_unknown_target(self):
result = normalize_googlechat_target("some_random_string")
assert result["type"] == "unknown"
class TestIsGooglechatUserTarget:
def test_users_prefix(self):
assert is_googlechat_user_target("users/user@x.com") is True
def test_user_colon_prefix(self):
assert is_googlechat_user_target("user:user@x.com") is True
def test_with_channel_prefix(self):
assert is_googlechat_user_target("googlechat:users/user@x.com") is True
def test_not_user_target(self):
assert is_googlechat_user_target("spaces/ABC") is False
def test_email_only(self):
assert is_googlechat_user_target("user@x.com") is False
class TestIsGooglechatSpaceTarget:
def test_spaces_prefix(self):
assert is_googlechat_space_target("spaces/ABC") is True
def test_not_space_target(self):
assert is_googlechat_space_target("users/user@x.com") is False
def test_with_channel_prefix(self):
assert is_googlechat_space_target("gchat:spaces/ABC") is True
class TestResolveTargets:
def test_all_targets(self):
results = resolve_targets(["spaces/ABC", "users/user@x.com", "spaces/XYZ/threads/t1"])
assert len(results) == 3
assert results[0]["type"] == "space"
assert results[1]["type"] == "user"
assert results[2]["type"] == "thread"
def test_filter_by_kind_space(self):
results = resolve_targets(
["spaces/ABC", "users/user@x.com", "spaces/XYZ"],
kind="space",
)
assert len(results) == 2
assert all(r["type"] == "space" for r in results)
def test_filter_by_kind_user(self):
results = resolve_targets(
["spaces/ABC", "users/user@x.com", "users/user2@y.com"],
kind="user",
)
assert len(results) == 2
assert all(r["type"] == "user" for r in results)
def test_empty_input(self):
assert resolve_targets([]) == []
def test_unknown_targets_filtered_by_kind(self):
results = resolve_targets(["unknown_str"], kind="space")
assert results == []
# ============================================================================
# threads.py tests
# ============================================================================
class TestParseThreadKey:
def test_has_thread_key(self):
msg = {"thread": {"threadKey": "thread-001"}}
assert parse_thread_key(msg) == "thread-001"
def test_no_thread_key(self):
msg = {"thread": {}}
assert parse_thread_key(msg) is None
def test_no_thread_field(self):
assert parse_thread_key({}) is None
class TestExtractThreadMetadata:
def test_full_metadata(self):
msg = {"thread": {"threadKey": "t1", "name": "spaces/ABC/threads/t1"}}
result = extract_thread_metadata(msg)
assert result["thread_key"] == "t1"
assert result["thread_name"] == "spaces/ABC/threads/t1"
assert result["is_thread_root"] is False
def test_is_thread_root(self):
msg = {"thread": {}}
result = extract_thread_metadata(msg)
assert result["is_thread_root"] is True
def test_no_thread_field(self):
result = extract_thread_metadata({})
assert result["thread_key"] is None
assert result["is_thread_root"] is True
class TestIsThreadMessage:
def test_is_thread(self):
assert is_thread_message({"thread": {"threadKey": "t1"}}) is True
def test_is_not_thread(self):
assert is_thread_message({"thread": {}}) is False
def test_no_thread_field(self):
assert is_thread_message({}) is False
# ============================================================================
# session.py tests
# ============================================================================
class TestBuildThreadId:
def test_space_session(self):
tid = build_thread_id("agent1", "spaces/ABC123")
assert tid == "agent:agent1:googlechat:space:ABC123"
def test_dm_session(self):
tid = build_thread_id("agent1", "spaces/DM_123", "user@example.com")
assert tid == "agent:agent1:googlechat:space:DM_123"
def test_empty_user_email(self):
tid = build_thread_id("agent1", "spaces/ABC123", "")
assert "space" in tid
# ============================================================================
# approval_auth.py tests
# ============================================================================
class TestNormalizeApproverId:
def test_empty(self):
assert normalize_approver_id("") == ""
def test_already_prefixed_users(self):
assert normalize_approver_id("users/user@x.com") == "users/user@x.com"
def test_already_prefixed_user_colon(self):
assert normalize_approver_id("user:user@x.com") == "user:user@x.com"
def test_email_without_prefix(self):
assert normalize_approver_id("user@example.com") == "users/user@example.com"
def test_plain_id(self):
assert normalize_approver_id("user123") == "users/user123"
class TestResolveApproverIds:
def test_normal_entries(self):
result = resolve_approver_ids(["user@a.com", "user@b.com"])
assert len(result) == 2
assert "users/user@a.com" in result
assert "users/user@b.com" in result
def test_skip_wildcard(self):
result = resolve_approver_ids(["*", "user@a.com"])
assert len(result) == 1
assert "users/user@a.com" in result
def test_empty_with_default(self):
result = resolve_approver_ids([], default_to="user@default.com")
assert result == ["users/user@default.com"]
def test_empty_no_default(self):
result = resolve_approver_ids([])
assert result == []
def test_all_wildcards(self):
result = resolve_approver_ids(["*"])
assert result == []
class TestBuildExecApprovalContext:
def test_basic_context(self):
ctx = build_exec_approval_context("action_1", ["approver1", "approver2"])
assert ctx["action_id"] == "action_1"
assert ctx["approvers"] == ["approver1", "approver2"]
assert ctx["approved_by"] == []
assert ctx["rejected_by"] == []
def test_with_metadata(self):
ctx = build_exec_approval_context("action_2", ["approver1"], {"key": "value"})
assert ctx["metadata"]["key"] == "value"
def test_default_metadata_is_dict(self):
ctx = build_exec_approval_context("action_3", [])
assert ctx["metadata"] == {}
class TestRecordApprovalDecision:
def test_approve_success(self):
ctx = build_exec_approval_context("a1", ["users/u1", "users/u2"])
assert record_approval_decision(ctx, "users/u1", approved=True) is True
assert "users/u1" in ctx["approved_by"]
def test_reject_success(self):
ctx = build_exec_approval_context("a1", ["users/u1"])
assert record_approval_decision(ctx, "users/u1", approved=False) is True
assert "users/u1" in ctx["rejected_by"]
def test_unauthorized_user(self):
ctx = build_exec_approval_context("a1", ["users/u1"])
assert record_approval_decision(ctx, "users/u2", approved=True) is False
def test_duplicate_approval(self):
ctx = build_exec_approval_context("a1", ["users/u1"])
record_approval_decision(ctx, "users/u1", approved=True)
assert record_approval_decision(ctx, "users/u1", approved=True) is True
assert len(ctx["approved_by"]) == 1
def test_duplicate_rejection(self):
ctx = build_exec_approval_context("a1", ["users/u1"])
record_approval_decision(ctx, "users/u1", approved=False)
assert record_approval_decision(ctx, "users/u1", approved=False) is True
assert len(ctx["rejected_by"]) == 1
def test_normalizes_user_id(self):
ctx = build_exec_approval_context("a1", ["users/u1@x.com"])
record_approval_decision(ctx, "u1@x.com", approved=True)
assert "users/u1@x.com" in ctx["approved_by"]
class TestIsFullyApproved:
def test_all_approved(self):
ctx = build_exec_approval_context("a1", ["u1", "u2"])
ctx["approved_by"] = ["u1", "u2"]
assert is_fully_approved(ctx) is True
def test_partial_approved(self):
ctx = build_exec_approval_context("a1", ["u1", "u2"])
ctx["approved_by"] = ["u1"]
assert is_fully_approved(ctx) is False
def test_none_approved(self):
ctx = build_exec_approval_context("a1", ["u1", "u2"])
assert is_fully_approved(ctx) is False
def test_empty_approvers(self):
ctx = build_exec_approval_context("a1", [])
assert is_fully_approved(ctx) is False
# ============================================================================
# directory.py tests
# ============================================================================
class TestNormalizeId:
def test_empty(self):
assert normalize_id("") == ""
def test_spaces_prefix(self):
assert normalize_id("spaces/ABC") == "spaces/ABC"
def test_users_prefix(self):
assert normalize_id("users/user@x.com") == "users/user@x.com"
def test_user_colon_prefix(self):
assert normalize_id("user:user@x.com") == "users/user@x.com"
def test_email(self):
assert normalize_id("user@example.com") == "users/user@example.com"
def test_plain_id(self):
assert normalize_id("someuser") == "users/someuser"
def test_whitespace_trimmed(self):
assert normalize_id(" users/abc ") == "users/abc"
class TestListPeers:
def test_normal_entries(self):
peers = list_peers(["user@a.com", "user@b.com"])
assert len(peers) == 2
assert peers[0]["type"] == "user"
assert peers[0]["id"] == "users/user@a.com"
def test_skip_wildcard(self):
peers = list_peers(["*", "user@a.com"])
assert len(peers) == 1
def test_empty_list(self):
assert list_peers([]) == []
class TestListGroups:
def test_empty_config(self):
assert list_groups({}) == []
def test_groups_with_config(self):
groups = list_groups({
"spaces/ABC": {"name": "General", "requireMention": True},
"spaces/XYZ": {"name": "Support", "enabled": False},
})
assert len(groups) == 2
assert groups[0]["id"] == "spaces/ABC"
assert groups[1]["requires_mention"] is False
def test_non_dict_entries_skipped(self):
groups = list_groups({
"spaces/ABC": "not_a_dict",
})
assert groups == []
# ============================================================================
# proxy.py tests
# ============================================================================
class TestProxyConfig:
def test_empty_config(self):
result = resolve_proxy_config()
assert result["http"] is None
assert result["https"] is None
def test_config_provided(self):
result = resolve_proxy_config({"httpProxy": "http://proxy:8080"})
assert result["http"] == "http://proxy:8080"
def test_https_fallback_to_http(self):
result = resolve_proxy_config({"httpProxy": "http://proxy:8080"})
assert result["https"] == "http://proxy:8080"
def test_env_var_fallback(self, monkeypatch):
monkeypatch.setenv("HTTP_PROXY", "http://env-proxy:3128")
result = resolve_proxy_config()
assert result["http"] == "http://env-proxy:3128"
def test_no_proxy_from_config(self):
result = resolve_proxy_config({"noProxy": "localhost,.local"})
assert result["no_proxy"] == "localhost,.local"
class TestBuildProxiesDict:
def test_empty_config(self):
assert build_proxies_dict({}) is None
def test_http_only(self):
result = build_proxies_dict({"http": "http://proxy:8080"})
assert result is not None
assert result["http"] == "http://proxy:8080"
def test_all_proxies(self):
result = build_proxies_dict({
"http": "http://proxy:8080",
"https": "https://proxy:8443",
"no_proxy": "localhost",
})
assert result is not None
assert result["http"] == "http://proxy:8080"
assert result["https"] == "https://proxy:8443"
assert result["no_proxy"] == "localhost"
class TestResolveTlsConfig:
def test_empty_config(self):
result = resolve_tls_config()
assert result["cert"] is None
assert result["key"] is None
def test_config_provided(self):
result = resolve_tls_config({"cert": "/path/cert.pem", "key": "/path/key.pem"})
assert result["cert"] == "/path/cert.pem"
assert result["key"] == "/path/key.pem"
def test_env_var_fallback(self, monkeypatch):
monkeypatch.setenv("GOOGLE_CHAT_TLS_CERT", "/env/cert.pem")
result = resolve_tls_config()
assert result["cert"] == "/env/cert.pem"
def test_snake_case_keys(self):
result = resolve_tls_config({"tlsCert": "/path/cert.pem", "tlsKey": "/path/key.pem"})
assert result["cert"] == "/path/cert.pem"
assert result["key"] == "/path/key.pem"
# ============================================================================
# ssrf.py tests
# ============================================================================
class TestSSRF:
def test_sanitize_google_auth_init(self):
result = sanitize_google_auth_init({"timeout": 30, "verify": False})
assert "timeout" in result
assert "verify" in result
def test_sanitize_request_kwargs(self):
result = sanitize_request_kwargs({"timeout": 10})
assert isinstance(result, dict)
def test_apply_ssrf_guard(self):
result = apply_ssrf_guard()
assert isinstance(result, dict)
# ============================================================================
# cards.py tests
# ============================================================================
class TestBuildApprovalCard:
def test_basic_structure(self):
card = build_approval_card("Approve?", "action_1")
assert "cards_v2" in card
assert len(card["cards_v2"]) == 1
assert card["cards_v2"][0]["card"]["header"]["title"] == "Approve?"
def test_custom_button_texts(self):
card = build_approval_card("Title", "action_1", confirm_text="Yes", reject_text="No")
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"][0]["buttonList"]["buttons"]
assert widgets[0]["text"] == "Yes"
assert widgets[1]["text"] == "No"
def test_has_two_buttons(self):
card = build_approval_card("Title", "action_1")
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"][0]["buttonList"]["buttons"]
assert len(widgets) == 2
class TestBuildPollCard:
def test_basic_poll(self):
card = build_poll_card("Question?", ["A", "B"], "poll_1")
assert "cards_v2" in card
assert card["cards_v2"][0]["card"]["header"]["title"] == "Question?"
def test_multiple_options(self):
card = build_poll_card("Q", ["A", "B", "C", "D"], "poll_1")
assert len(card["cards_v2"][0]["card"]["sections"]) >= 2
class TestBuildInfoCard:
def test_basic_info_card(self):
card = build_info_card("Info", {"Key1": "Val1", "Key2": "Val2"})
assert "cards_v2" in card
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert len(widgets) == 2
def test_decorated_text_structure(self):
card = build_info_card("Title", {"Key": "Value"})
widget = card["cards_v2"][0]["card"]["sections"][0]["widgets"][0]
assert widget["decoratedText"]["topLabel"] == "Key"
assert widget["decoratedText"]["text"] == "Value"
class TestBuildFormCard:
def test_text_input_field(self):
card = build_form_card(
"Form",
[{"name": "email", "label": "Email", "input_type": "SINGLE_LINE"}],
"submit_1",
)
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert widgets[0]["textInput"]["name"] == "email"
def test_selection_field(self):
card = build_form_card(
"Form",
[{"name": "country", "label": "Country", "input_type": "SINGLE_SELECT", "options": [{"text": "CN", "value": "cn"}]}],
"submit_1",
)
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert widgets[0]["selectionInput"]["name"] == "country"
def test_date_picker_field(self):
card = build_form_card(
"Form",
[{"name": "date", "label": "Date", "input_type": "DATE_AND_TIME"}],
"submit_1",
)
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert "dateTimePicker" in widgets[0]
def test_divider_field(self):
card = build_form_card(
"Form",
[
{"name": "email", "label": "Email", "input_type": "SINGLE_LINE"},
{"input_type": "DIVIDER"},
],
"submit_1",
)
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert "divider" in widgets[1]
def test_has_submit_button(self):
card = build_form_card("Form", [{"name": "x", "label": "X"}], "submit_1")
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
last = widgets[-1]
assert "buttonList" in last
class TestBuildExecApprovalCard:
def test_basic_structure(self):
card = build_exec_approval_card("Execute?", "exec_1")
assert "cards_v2" in card
def test_with_detail_fields(self):
card = build_exec_approval_card("Execute?", "exec_1", {"Module": "Payments"})
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert widgets[0]["decoratedText"]["topLabel"] == "Module"
def test_has_two_buttons(self):
card = build_exec_approval_card("Execute?", "exec_1")
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
buttons = widgets[-1]["buttonList"]["buttons"]
assert len(buttons) == 2
class TestBuildSelectionCard:
def test_basic_structure(self):
card = build_selection_card(
"Choose",
"select_1",
"Pick one",
[{"text": "Option A", "value": "a"}],
)
assert "cards_v2" in card
widget = card["cards_v2"][0]["card"]["sections"][0]["widgets"][0]
assert widget["selectionInput"]["type"] == "SINGLE_SELECT"
def test_multi_select(self):
card = build_selection_card(
"Choose",
"select_1",
"Pick",
[{"text": "A", "value": "a"}],
selection_type="MULTI_SELECT",
)
widget = card["cards_v2"][0]["card"]["sections"][0]["widgets"][0]
assert widget["selectionInput"]["type"] == "MULTI_SELECT"
def test_with_submit_button(self):
card = build_selection_card(
"Choose", "select_1", "Pick", [{"text": "A", "value": "a"}],
submit_action="submit_1",
)
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert len(widgets) == 2
assert "buttonList" in widgets[1]
def test_no_submit_button(self):
card = build_selection_card(
"Choose", "select_1", "Pick", [{"text": "A", "value": "a"}],
)
widgets = card["cards_v2"][0]["card"]["sections"][0]["widgets"]
assert len(widgets) == 1
# ============================================================================
# streaming.py tests
# ============================================================================
class TestStreamManager:
@pytest.fixture
def stream_mgr(self):
return StreamManager()
def test_initial_state(self, stream_mgr):
assert stream_mgr.pending_count == 0
assert stream_mgr.has_pending("any") is False
assert stream_mgr.get_message_name("any") is None
assert stream_mgr.get_accumulated_text("any") == ""
@pytest.mark.asyncio
async def test_register_first_chunk(self, stream_mgr):
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
assert stream_mgr.has_pending("chat1") is True
assert stream_mgr.get_message_name("chat1") == "msg1"
assert stream_mgr.get_accumulated_text("chat1") == "Hello"
@pytest.mark.asyncio
async def test_append_text(self, stream_mgr):
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
result = await stream_mgr.append_text("chat1", " World")
assert result == "Hello World"
assert stream_mgr.get_accumulated_text("chat1") == "Hello World"
@pytest.mark.asyncio
async def test_append_to_nonexistent(self, stream_mgr):
result = await stream_mgr.append_text("new_chat", "First")
assert result == "First"
def test_should_update_initial(self, stream_mgr):
assert stream_mgr.should_update("chat1") is True
@pytest.mark.asyncio
async def test_should_update_after_mark(self, stream_mgr):
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
await stream_mgr.mark_update("chat1")
assert stream_mgr.should_update("chat1") is False
@pytest.mark.asyncio
async def test_should_update_after_interval(self, stream_mgr):
stream_mgr._update_interval_ms = 1
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
await stream_mgr.mark_update("chat1")
await asyncio.sleep(0.01)
assert stream_mgr.should_update("chat1") is True
@pytest.mark.asyncio
async def test_send_update_clears_on_finished(self, stream_mgr):
mock_service = MagicMock()
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
await stream_mgr.append_text("chat1", " World")
with patch(
"yuxi.channels.adapters.googlechat.streaming.update_message",
new_callable=AsyncMock,
return_value=MagicMock(success=True),
):
result = await stream_mgr.send_update(mock_service, "chat1", finished=True)
assert stream_mgr.has_pending("chat1") is False
@pytest.mark.asyncio
async def test_send_update_not_finished_keeps_pending(self, stream_mgr):
mock_service = MagicMock()
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
with patch(
"yuxi.channels.adapters.googlechat.streaming.update_message",
new_callable=AsyncMock,
return_value=MagicMock(success=True),
):
result = await stream_mgr.send_update(mock_service, "chat1", finished=False)
assert stream_mgr.has_pending("chat1") is True
@pytest.mark.asyncio
async def test_send_update_no_message(self, stream_mgr):
mock_service = MagicMock()
result = await stream_mgr.send_update(mock_service, "nonexistent")
assert result is None
@pytest.mark.asyncio
async def test_finalize(self, stream_mgr):
mock_service = MagicMock()
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
with patch(
"yuxi.channels.adapters.googlechat.streaming.update_message",
new_callable=AsyncMock,
return_value=MagicMock(success=True, message_id="msg1"),
):
result = await stream_mgr.finalize(mock_service, "chat1")
assert stream_mgr.has_pending("chat1") is False
@pytest.mark.asyncio
async def test_finalize_no_pending(self, stream_mgr):
mock_service = MagicMock()
result = await stream_mgr.finalize(mock_service, "nonexistent")
assert result.success is False
@pytest.mark.asyncio
async def test_handle_error_cleanup(self, stream_mgr):
mock_service = MagicMock()
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
await stream_mgr.handle_error(mock_service, "chat1")
assert stream_mgr.has_pending("chat1") is False
@pytest.mark.asyncio
async def test_clear(self, stream_mgr):
await stream_mgr.register_first_chunk("chat1", "msg1", "Hello")
await stream_mgr.register_first_chunk("chat2", "msg2", "World")
await stream_mgr.clear()
assert stream_mgr.pending_count == 0
@pytest.mark.asyncio
async def test_concurrent_registration(self, stream_mgr):
mgr = stream_mgr
await asyncio.gather(
mgr.register_first_chunk("chat1", "msg1", "A"),
mgr.register_first_chunk("chat2", "msg2", "B"),
mgr.register_first_chunk("chat3", "msg3", "C"),
)
assert mgr.pending_count == 3
# ============================================================================
# msg_cache.py tests
# ============================================================================
class TestSentMessageCache:
def test_put_and_get(self):
cache = SentMessageCache()
cache.put("msg1", {"chat_id": "chat1"})
result = cache.get("msg1")
assert result is not None
assert result["chat_id"] == "chat1"
def test_get_missing(self):
cache = SentMessageCache()
assert cache.get("nonexistent") is None
def test_expired_entry(self):
cache = SentMessageCache(max_age_s=0)
cache.put("msg1", {"chat_id": "chat1"})
assert cache.get("msg1") is None
def test_remove(self):
cache = SentMessageCache()
cache.put("msg1")
cache.remove("msg1")
assert cache.get("msg1") is None
def test_update_existing(self):
cache = SentMessageCache()
cache.put("msg1", {"chat_id": "chat1"})
cache.update("msg1", {"status": "delivered"})
result = cache.get("msg1")
assert result["chat_id"] == "chat1"
assert result["status"] == "delivered"
def test_update_missing(self):
cache = SentMessageCache()
cache.update("nonexistent", {"status": "delivered"})
def test_size(self):
cache = SentMessageCache(max_size=5)
for i in range(3):
cache.put(f"msg{i}")
assert cache.size() == 3
def test_max_size_eviction(self):
cache = SentMessageCache(max_size=3)
for i in range(5):
cache.put(f"msg{i}")
assert cache.size() == 3
def test_clear(self):
cache = SentMessageCache()
for i in range(5):
cache.put(f"msg{i}")
cache.clear()
assert cache.size() == 0
def test_expired_count(self):
cache = SentMessageCache(max_age_s=0)
cache.put("msg1")
cache.put("msg2")
assert cache.expired_count() == 2
def test_expired_count_none_expired(self):
cache = SentMessageCache(max_age_s=3600)
cache.put("msg1")
assert cache.expired_count() == 0
def test_put_with_no_metadata(self):
cache = SentMessageCache()
cache.put("msg1")
result = cache.get("msg1")
assert result == {}
class TestTrackSentMessage:
def test_tracks_message(self):
cache = SentMessageCache()
track_sent_message(cache, "msg1", "chat1", "text")
result = cache.get("msg1")
assert result is not None
assert result["chat_id"] == "chat1"
assert result["message_type"] == "text"
class TestUpdateSentMessageStatus:
def test_updates_status(self):
cache = SentMessageCache()
cache.put("msg1", {"chat_id": "chat1"})
update_sent_message_status(cache, "msg1", "delivered")
result = cache.get("msg1")
assert result["status"] == "delivered"
def test_updates_with_metadata(self):
cache = SentMessageCache()
cache.put("msg1")
update_sent_message_status(cache, "msg1", "failed", {"error": "timeout"})
result = cache.get("msg1")
assert result["status"] == "failed"
assert result["error"] == "timeout"
# ============================================================================
# doctor.py tests
# ============================================================================
class TestRunDiagnostics:
def test_empty_config(self):
assert run_diagnostics(None) == []
assert run_diagnostics({}) == []
def test_credential_missing_error(self):
issues = run_diagnostics({"some": "config"})
cred_issues = [i for i in issues if i["category"] == "credential_readiness"]
assert len(cred_issues) >= 1
assert cred_issues[0]["severity"] == "error"
def test_deprecated_stream_mode(self):
issues = run_diagnostics({"streamMode": "on"})
dep_issues = [i for i in issues if i["field"] == "streamMode"]
assert len(dep_issues) >= 1
def test_deprecated_enable_thread_reply(self):
issues = run_diagnostics({"enableThreadReply": True})
dep_issues = [i for i in issues if i["field"] == "enableThreadReply"]
assert len(dep_issues) >= 1
def test_mutable_allowlist_warning(self):
issues = run_diagnostics({"allowFrom": ["user@example.com"]})
mutable = [i for i in issues if i["category"] == "mutable_allowlist"]
assert len(mutable) >= 1
def test_credential_readiness_with_service_account(self):
issues = run_diagnostics({"service_account": "{}"})
cred_issues = [i for i in issues if i["category"] == "credential_readiness"]
assert len(cred_issues) == 0
# ============================================================================
# setup.py tests
# ============================================================================
class TestWizardValidateServiceAccount:
def test_valid_credentials(self):
result = wizard_validate_service_account({
"type": "service_account",
"private_key": "-----BEGIN PRIVATE KEY-----\nxxx\n-----END PRIVATE KEY-----\n",
"client_email": "sa@project.iam.gserviceaccount.com",
"token_uri": "https://oauth2.googleapis.com/token",
})
assert result["valid"] is True
assert result["errors"] == []
def test_invalid_type(self):
result = wizard_validate_service_account({"type": "authorized_user"})
assert result["valid"] is False
assert any("type" in e for e in result["errors"])
def test_missing_private_key(self):
result = wizard_validate_service_account({
"type": "service_account",
"client_email": "sa@project.iam.gserviceaccount.com",
"token_uri": "https://oauth2.googleapis.com/token",
})
assert result["valid"] is False
assert any("private_key" in e for e in result["errors"])
def test_missing_client_email(self):
result = wizard_validate_service_account({
"type": "service_account",
"private_key": "key",
"token_uri": "https://oauth2.googleapis.com/token",
})
assert result["valid"] is False
assert any("client_email" in e for e in result["errors"])
def test_missing_token_uri(self):
result = wizard_validate_service_account({
"type": "service_account",
"private_key": "key",
"client_email": "sa@project.iam.gserviceaccount.com",
})
assert result["valid"] is False
assert any("token_uri" in e for e in result["errors"])
class TestSetupWizard:
def test_start(self):
wizard = SetupWizard()
result = wizard.start()
assert result["step"] == 0
assert result["steps"] == 5
def test_next_advances(self):
wizard = SetupWizard()
wizard.start()
result = wizard.next("env_file")
assert result["step"] == 1
def test_full_flow(self):
wizard = SetupWizard()
wizard.start()
wizard.next("env_file")
wizard.next(None)
wizard.next({"audience_type": "app-url", "audience": "https://example.com"})
wizard.next({"dm_policy": "open", "group_policy": "open", "require_mention": False})
result = wizard.next(None)
assert result["done"] is True
assert "config" in result
assert result["config"]["enabled"] is True
def test_inline_credential_flow(self):
wizard = SetupWizard()
wizard.start()
wizard.next("inline")
wizard.next('{"type":"service_account"}')
wizard.next({"audience_type": "app-url"})
wizard.next({"dm_policy": "open"})
result = wizard.next(None)
assert result["done"] is True
assert result["config"].get("serviceAccount") == '{"type":"service_account"}'
def test_current_step(self):
wizard = SetupWizard()
wizard.start()
step = wizard.current_step
assert step is not None
assert "id" in step
def test_current_step_none_after_finish(self):
wizard = SetupWizard()
wizard.start()
for _ in range(5):
wizard.next()
assert wizard.current_step is None
class TestGetSetupGuide:
def test_returns_non_empty_string(self):
guide = get_setup_guide()
assert len(guide) > 100
assert "Google Cloud Console" in guide
# ============================================================================
# secret_contract.py tests
# ============================================================================
class TestSecretContracts:
def test_get_secret_contracts(self):
contracts = get_secret_contracts()
assert "serviceAccount" in contracts
assert contracts["serviceAccount"]["required"] is True
assert contracts["serviceAccount"]["sensitive"] is True
def test_get_required_secrets(self):
required = get_required_secrets()
assert "serviceAccount" in required
assert "serviceAccountFile" not in required
def test_get_sensitive_fields(self):
sensitive = get_sensitive_fields()
assert "serviceAccount" in sensitive
assert "webhookSecret" in sensitive
def test_is_sensitive_field(self):
assert is_sensitive_field("serviceAccount") is True
assert is_sensitive_field("some_key") is True
assert is_sensitive_field("my_secret") is True
assert is_sensitive_field("normal_field") is False
def test_register_secret_contract(self):
register_secret_contract("testContract", {"required": False, "sensitive": True})
contracts = get_secret_contracts()
assert "testContract" in contracts
def test_contracts_is_a_copy(self):
original = get_secret_contracts()
original["modified"] = {"required": False}
fresh = get_secret_contracts()
assert "modified" not in fresh
# ============================================================================
# auth.py tests
# ============================================================================
class TestGoogleChatCertCache:
def test_initial_state(self):
cache = GoogleChatCertCache()
assert cache.is_valid is False
@pytest.mark.asyncio
async def test_get_certs_returns_dict(self):
cache = GoogleChatCertCache()
with patch("httpx.AsyncClient.get") as mock_get:
mock_get.return_value = MagicMock(status_code=200, json=MagicMock(return_value={"key1": "cert1"}))
mock_get.return_value.raise_for_status = MagicMock()
mock_get.return_value.json = MagicMock(return_value={"key1": "cert1"})
certs = await cache.get_certs()
assert isinstance(certs, dict)
assert "key1" in certs
@pytest.mark.asyncio
async def test_get_certs_empty_on_error_no_cache(self):
cache = GoogleChatCertCache()
with patch("httpx.AsyncClient.get", side_effect=Exception("network error")):
certs = await cache.get_certs()
assert certs == {}
@pytest.mark.asyncio
async def test_get_certs_returns_stale_on_error(self):
cache = GoogleChatCertCache()
cache._cache = {"key1": "cert1"}
cache._cache_at = time.monotonic() - 100
with patch("httpx.AsyncClient.get", side_effect=Exception("network error")):
certs = await cache.get_certs()
assert certs == {"key1": "cert1"}
def test_clear(self):
cache = GoogleChatCertCache()
cache._cache = {"key1": "cert1"}
cache._cache_at = time.monotonic()
cache.clear()
assert cache.is_valid is False
assert cache._cache is None
def test_is_valid_within_ttl(self):
cache = GoogleChatCertCache(ttl_s=600)
cache._cache = {"key1": "cert1"}
cache._cache_at = time.monotonic()
assert cache.is_valid is True
def test_is_valid_expired(self):
cache = GoogleChatCertCache(ttl_s=0)
cache._cache = {"key1": "cert1"}
cache._cache_at = time.monotonic() - 1
assert cache.is_valid is False
class TestVerifyProjectNumberToken:
def test_no_certs(self):
assert verify_project_number_token("token", "123", {}) is False
def test_without_google_auth_installed(self):
import google.auth
if hasattr(google.auth, "jwt"):
pytest.skip("google.auth.jwt is installed — ImportError branch not testable")
assert verify_project_number_token("token", "123", {"key1": "cert1"}) is True
def test_invalid_token(self):
with patch("google.auth.jwt.decode", side_effect=ValueError("invalid")):
assert verify_project_number_token("token", "123", {"key1": "cert1"}) is False
class TestGetCertCache:
def test_singleton(self):
cache1 = get_cert_cache()
cache2 = get_cert_cache()
assert cache1 is cache2