Files
replypier/backend/tests/channels/test_app_hooks.py
T
WangShen007 e0baca15a9 feat(modules): 帮助中心、工单、工作流与多渠道接入
补齐 FAQ、帮助中心、工单、可视化工作流及 Telegram/WhatsApp/Slack 等渠道模块。
2026-10-17 22:05:00 +08:00

132 lines
4.5 KiB
Python

"""
Copyright 2024-2026 ReplyPier
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.
"""
import asyncio
from datetime import datetime, timezone
from unittest.mock import AsyncMock
from uuid import uuid4
import pytest
from app.channels import app_hooks
from app.channels.app_hooks import (
ChannelEvent, EventKind, MessageSource, OutboundRecord, emit_channel_event,
inbound_event_id, outbound_event, register_event_listener, register_inbound_handler,
)
def _event() -> ChannelEvent:
return ChannelEvent(kind=EventKind.INBOUND, event_id="a:1", organization_id=uuid4(),
channel_account_id=uuid4(), occurred_at=datetime.now(timezone.utc))
@pytest.mark.asyncio
async def test_no_extension_means_every_hook_is_a_no_op():
assert app_hooks.inbound_handler() is None
await emit_channel_event(_event()) # nothing listening, nothing raised
@pytest.mark.asyncio
async def test_a_failing_listener_never_stops_the_others():
calls = []
async def broken(event):
raise RuntimeError("extension bug")
async def working(event):
calls.append(event.event_id)
register_event_listener(broken)
register_event_listener(working)
register_event_listener(working) # registering twice is a no-op
await emit_channel_event(_event())
assert calls == ["a:1"]
def test_load_extensions_without_one_installed_is_harmless(monkeypatch):
monkeypatch.setattr(app_hooks, "EXTENSION_HOOKS_MODULE", "app.no_such_extension_module")
app_hooks.load_extensions()
app_hooks.load_extensions() # idempotent
assert app_hooks.inbound_handler() is None
def test_load_extensions_imports_the_extension_once(monkeypatch):
imported = []
monkeypatch.setattr(app_hooks.importlib, "import_module", lambda name: imported.append(name))
app_hooks.load_extensions()
app_hooks.load_extensions()
assert imported == [app_hooks.EXTENSION_HOOKS_MODULE]
def test_reset_forgets_registrations():
register_inbound_handler(AsyncMock(return_value=True))
register_event_listener(AsyncMock())
app_hooks.reset()
assert app_hooks.inbound_handler() is None
def test_event_ids_are_stable_for_the_same_message():
account = uuid4()
assert inbound_event_id(account, "wamid.1") == inbound_event_id(account, "wamid.1")
record = OutboundRecord(message_id=7, external_message_id="wamid.OUT",
source=MessageSource.AI, text="hi")
first = outbound_event(organization_id=uuid4(), channel_account_id=account,
session_id="s", customer_id=None, record=record)
second = outbound_event(organization_id=uuid4(), channel_account_id=account,
session_id="s", customer_id=None, record=record)
assert first.event_id == second.event_id == f"{account}:out:wamid.OUT"
def test_outbound_without_a_platform_id_is_keyed_by_its_row():
account = uuid4()
record = OutboundRecord(message_id=7, external_message_id=None,
source=MessageSource.SYSTEM, text="hi")
event = outbound_event(organization_id=uuid4(), channel_account_id=account,
session_id=None, customer_id=None, record=record)
assert event.event_id == f"{account}:out:row7"
def test_a_broken_extension_is_reported_not_silently_skipped(monkeypatch, caplog):
def import_module(name):
raise ModuleNotFoundError("No module named 'some_dependency'", name="some_dependency")
monkeypatch.setattr(app_hooks.importlib, "import_module", import_module)
with caplog.at_level("ERROR"):
app_hooks.load_extensions()
assert "Channel extension failed to load" in caplog.text
@pytest.mark.asyncio
async def test_a_slow_listener_is_cut_off(monkeypatch):
monkeypatch.setattr(app_hooks, "LISTENER_TIMEOUT_SECONDS", 0.01)
calls = []
async def slow(event):
await asyncio.sleep(5)
async def fast(event):
calls.append(event.event_id)
register_event_listener(slow)
register_event_listener(fast)
await emit_channel_event(_event())
assert calls == ["a:1"]