ForcePilot/backend/test/unit/channels/mock_nextcloudtalk_api.py

171 lines
6.7 KiB
Python
Raw Normal View History

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({}))