test: 补充权限、消息与会话服务回归用例
会话读取、接管、重分配与 Socket 订阅统一校验角色、组织与可见性。
This commit is contained in:
1 parent
5da5f910da
commit
2554eb7bc7
97 files changed
+37281
No files matched your search
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.channels.base import ChannelInteraction, InboundMessage
|
||||
from app.models.customer import Customer
|
||||
from app.repositories.channels import (
|
||||
ChannelAccountRepository,
|
||||
ChannelConversationRepository,
|
||||
AgentChannelConfigRepository,
|
||||
)
|
||||
from app.repositories.rating import RatingRepository
|
||||
from app.repositories.session_to_agent import SessionToAgentRepository
|
||||
from app.services import channel_chat
|
||||
from app.services.channel_chat import process_channel_interaction, process_channel_message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def use_test_db(db, monkeypatch):
|
||||
class _NonClosing:
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
def close(self):
|
||||
pass
|
||||
monkeypatch.setattr(channel_chat, "SessionLocal", lambda: _NonClosing(db))
|
||||
return db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def account(db, test_organization):
|
||||
return ChannelAccountRepository(db).create_account(
|
||||
organization_id=test_organization.id,
|
||||
channel_type="telegram",
|
||||
external_account_id="bot1",
|
||||
credentials={"bot_token": "1:x"},
|
||||
display_name="@bot",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conversation(db, account, test_agent, test_customer, test_organization):
|
||||
import uuid
|
||||
session = SessionToAgentRepository(db).create_session(
|
||||
session_id=uuid.uuid4(),
|
||||
agent_id=test_agent.id, customer_id=test_customer.id,
|
||||
organization_id=test_organization.id, channel="telegram")
|
||||
return ChannelConversationRepository(db).create(
|
||||
channel_account_id=account.id, channel_type="telegram",
|
||||
external_conversation_id="555", external_user_id="555",
|
||||
session_id=session.session_id, organization_id=test_organization.id,
|
||||
agent_id=test_agent.id, customer_id=test_customer.id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_adapter(monkeypatch):
|
||||
adapter = MagicMock()
|
||||
adapter.send_text = AsyncMock()
|
||||
adapter.send_typing = AsyncMock()
|
||||
adapter.request_phone = AsyncMock()
|
||||
monkeypatch.setattr(channel_chat, "get_adapter", lambda ct: adapter)
|
||||
return adapter
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contact_stores_phone(db, account, conversation, use_test_db, fake_adapter):
|
||||
interaction = ChannelInteraction(type="contact", external_account_id="",
|
||||
external_conversation_id="555", external_user_id="555",
|
||||
phone="+441234567890")
|
||||
await process_channel_interaction(account.id, interaction)
|
||||
|
||||
customer = db.query(Customer).filter(Customer.id == conversation.customer_id).one()
|
||||
assert customer.meta_data.get("phone") == "+441234567890"
|
||||
# Promoted to the identity column too, so the person is phone-addressable
|
||||
assert customer.phone == "+441234567890"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_typing_shown_no_rating_prompt_on_end_chat(db, account, conversation, use_test_db,
|
||||
fake_adapter, test_ai_config):
|
||||
"""Typing indicator fires, but rating is NEVER asked on a channel — rating
|
||||
is a web-widget-only feature."""
|
||||
AgentChannelConfigRepository(db).set_agent(account.id, conversation.agent_id)
|
||||
response = SimpleNamespace(message="Bye!", transfer_to_human=False, end_chat=True,
|
||||
request_rating=True, request_contact=False,
|
||||
request_lead_capture=False)
|
||||
fake = MagicMock()
|
||||
fake.get_response = AsyncMock(return_value=response)
|
||||
fake.safe_cleanup_mcp_tools = AsyncMock()
|
||||
|
||||
inbound = InboundMessage(external_account_id="", external_conversation_id="555",
|
||||
external_user_id="555", external_message_id="m3", text="bye")
|
||||
with patch.object(channel_chat.ChatAgent, "create_async", AsyncMock(return_value=fake)), \
|
||||
patch.object(channel_chat, "deliver_to_customer", AsyncMock()):
|
||||
await process_channel_message(account.id, inbound)
|
||||
|
||||
fake_adapter.send_typing.assert_awaited()
|
||||
# No rating was recorded and the adapter was never asked to prompt for one
|
||||
assert RatingRepository(db).get_rating_by_session(conversation.session_id) is None
|
||||
assert not hasattr(fake_adapter, "send_rating_prompt") or \
|
||||
not fake_adapter.send_rating_prompt.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_contact_triggers_phone_prompt(db, account, conversation, use_test_db,
|
||||
fake_adapter, test_ai_config):
|
||||
AgentChannelConfigRepository(db).set_agent(account.id, conversation.agent_id)
|
||||
response = SimpleNamespace(message="Let me connect you.", transfer_to_human=False,
|
||||
end_chat=False, request_rating=False, request_contact=True,
|
||||
request_lead_capture=False)
|
||||
fake = MagicMock()
|
||||
fake.get_response = AsyncMock(return_value=response)
|
||||
fake.safe_cleanup_mcp_tools = AsyncMock()
|
||||
|
||||
inbound = InboundMessage(external_account_id="", external_conversation_id="555",
|
||||
external_user_id="555", external_message_id="m4", text="help")
|
||||
with patch.object(channel_chat.ChatAgent, "create_async", AsyncMock(return_value=fake)), \
|
||||
patch.object(channel_chat, "deliver_to_customer", AsyncMock()):
|
||||
await process_channel_message(account.id, inbound)
|
||||
|
||||
fake_adapter.request_phone.assert_awaited()
|
||||
Reference in new issue
Block a user