新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
171 lines
6.7 KiB
Python
171 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from collections import defaultdict
|
|
from typing import Any
|
|
|
|
from aiohttp import web
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_MOCK_API_PORT = 18999
|
|
|
|
|
|
class MockNextcloudTalkAPI:
|
|
"""Simulates Nextcloud Talk OCS API for integration testing."""
|
|
|
|
def __init__(self, port: int = DEFAULT_MOCK_API_PORT):
|
|
self._port = port
|
|
self._app: web.Application | None = None
|
|
self._runner: web.AppRunner | None = None
|
|
self._site: web.TCPSite | None = None
|
|
|
|
self._conversations: dict[str, dict[str, Any]] = {}
|
|
self._messages: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
self._reactions: dict[str, dict[str, Any]] = {}
|
|
self._polls: dict[str, dict[str, Any]] = {}
|
|
self._msg_id_counter: int = 1000
|
|
|
|
self._capabilities = {
|
|
"ocs": {
|
|
"meta": {"status": "ok", "statuscode": 200},
|
|
"data": {
|
|
"version": {"string": "20.0.0", "edition": "Enterprise"},
|
|
"capabilities": {"spreed": {"version": "20.0.0"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
@property
|
|
def port(self) -> int:
|
|
return self._port
|
|
|
|
@property
|
|
def base_url(self) -> str:
|
|
return f"http://127.0.0.1:{self._port}"
|
|
|
|
def _ocs_ok(self, data: Any = None) -> dict[str, Any]:
|
|
return {"ocs": {"meta": {"status": "ok", "statuscode": 200}, "data": data or {}}}
|
|
|
|
def _ocs_created(self, data: Any = None) -> dict[str, Any]:
|
|
return {"ocs": {"meta": {"status": "ok", "statuscode": 201}, "data": data or {}}}
|
|
|
|
def add_conversation(self, token: str, conv_type: int = 2, name: str = "") -> None:
|
|
self._conversations[token] = {
|
|
"token": token,
|
|
"type": conv_type,
|
|
"name": name,
|
|
"displayName": name,
|
|
}
|
|
|
|
def add_message(self, token: str, message: dict[str, Any]) -> int:
|
|
msg_id = self._msg_id_counter
|
|
self._msg_id_counter += 1
|
|
self._messages[token].append({"id": msg_id, **message})
|
|
return msg_id
|
|
|
|
async def start(self) -> None:
|
|
self._app = web.Application()
|
|
self._app.router.add_get("/ocs/v2.php/cloud/capabilities", self._handle_capabilities)
|
|
self._app.router.add_get("/ocs/v2.php/apps/spreed/api/v4/conversation", self._handle_conversation_list)
|
|
self._app.router.add_post("/ocs/v2.php/apps/spreed/api/v4/chat/{token}", self._handle_send_chat)
|
|
self._app.router.add_put("/ocs/v2.php/apps/spreed/api/v4/chat/{token}/{msg_id}", self._handle_edit_chat)
|
|
self._app.router.add_delete("/ocs/v2.php/apps/spreed/api/v4/chat/{token}/{msg_id}", self._handle_delete_chat)
|
|
self._app.router.add_post("/ocs/v2.php/apps/spreed/api/v4/reaction/{token}/{msg_id}", self._handle_reaction)
|
|
self._app.router.add_delete("/ocs/v2.php/apps/spreed/api/v4/reaction/{token}/{msg_id}", self._handle_reaction_delete)
|
|
self._app.router.add_get("/ocs/v2.php/apps/spreed/api/v4/reaction/{token}/{msg_id}", self._handle_get_reactions)
|
|
self._app.router.add_post("/ocs/v2.php/apps/spreed/api/v4/poll/{token}", self._handle_create_poll)
|
|
self._app.router.add_get("/{tail:.*}", self._handle_fallback)
|
|
|
|
self._runner = web.AppRunner(self._app)
|
|
await self._runner.setup()
|
|
self._site = web.TCPSite(self._runner, "127.0.0.1", self._port)
|
|
await self._site.start()
|
|
logger.info(f"[MockNC] Mock API started on {self.base_url}")
|
|
|
|
async def stop(self) -> None:
|
|
if self._runner:
|
|
await self._runner.cleanup()
|
|
self._runner = None
|
|
self._site = None
|
|
self._app = None
|
|
logger.info("[MockNC] Mock API stopped")
|
|
|
|
async def _handle_capabilities(self, request: web.Request) -> web.Response:
|
|
return web.json_response(self._capabilities)
|
|
|
|
async def _handle_conversation_list(self, request: web.Request) -> web.Response:
|
|
return web.json_response(self._ocs_ok(list(self._conversations.values())))
|
|
|
|
async def _handle_send_chat(self, request: web.Request) -> web.Response:
|
|
token = request.match_info["token"]
|
|
try:
|
|
raw = await request.json()
|
|
except Exception:
|
|
raw = {}
|
|
msg_id = self._msg_id_counter
|
|
self._msg_id_counter += 1
|
|
return web.json_response(self._ocs_created({"id": msg_id, "token": token, "message": raw.get("message", "")}))
|
|
|
|
async def _handle_edit_chat(self, request: web.Request) -> web.Response:
|
|
return web.json_response(self._ocs_ok({}))
|
|
|
|
async def _handle_delete_chat(self, request: web.Request) -> web.Response:
|
|
return web.json_response(self._ocs_ok({}))
|
|
|
|
async def _handle_reaction(self, request: web.Request) -> web.Response:
|
|
token = request.match_info["token"]
|
|
msg_id = request.match_info["msg_id"]
|
|
try:
|
|
raw = await request.json()
|
|
except Exception:
|
|
raw = {}
|
|
key = f"{token}:{msg_id}"
|
|
current = self._reactions.get(key, {})
|
|
emoji = raw.get("reaction", "")
|
|
current[emoji] = current.get(emoji, 0) + 1
|
|
self._reactions[key] = current
|
|
return web.json_response(self._ocs_ok(current))
|
|
|
|
async def _handle_reaction_delete(self, request: web.Request) -> web.Response:
|
|
token = request.match_info["token"]
|
|
msg_id = request.match_info["msg_id"]
|
|
try:
|
|
raw = await request.json()
|
|
except Exception:
|
|
raw = {}
|
|
key = f"{token}:{msg_id}"
|
|
current = self._reactions.get(key, {})
|
|
emoji = raw.get("reaction", "")
|
|
if current.get(emoji, 0) > 0:
|
|
current[emoji] -= 1
|
|
if current[emoji] <= 0:
|
|
current.pop(emoji, None)
|
|
self._reactions[key] = current
|
|
return web.json_response(self._ocs_ok(current))
|
|
|
|
async def _handle_get_reactions(self, request: web.Request) -> web.Response:
|
|
token = request.match_info["token"]
|
|
msg_id = request.match_info["msg_id"]
|
|
key = f"{token}:{msg_id}"
|
|
return web.json_response(self._ocs_ok(self._reactions.get(key, {})))
|
|
|
|
async def _handle_create_poll(self, request: web.Request) -> web.Response:
|
|
token = request.match_info["token"]
|
|
try:
|
|
raw = await request.json()
|
|
except Exception:
|
|
raw = {}
|
|
poll_id = self._msg_id_counter
|
|
self._msg_id_counter += 1
|
|
self._polls[str(poll_id)] = {
|
|
"id": poll_id,
|
|
"token": token,
|
|
"question": raw.get("question", ""),
|
|
"options": raw.get("options", []),
|
|
}
|
|
return web.json_response(self._ocs_created({"id": poll_id}))
|
|
|
|
async def _handle_fallback(self, request: web.Request) -> web.Response:
|
|
return web.json_response(self._ocs_ok({})) |