Проект разделен на два репозитория
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import base64
|
||||
import os
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
os.environ.setdefault("BITRIX_DATABASE_URL", "postgresql://unused/unused")
|
||||
os.environ.setdefault("BITRIX_CLIENT_ID", "client")
|
||||
os.environ.setdefault("BITRIX_CLIENT_SECRET", "secret")
|
||||
os.environ.setdefault("BITRIX_APPLICATION_TOKEN", "application-token")
|
||||
os.environ.setdefault("BITRIX_INTERNAL_API_TOKEN", "internal-token-32-characters-long")
|
||||
os.environ.setdefault("BITRIX_API_FORWARD_URL", "http://api/internal/openlines/v1/inbox")
|
||||
os.environ.setdefault("BITRIX_API_FORWARD_TOKEN", "forward-token-32-characters-long")
|
||||
os.environ.setdefault(
|
||||
"BITRIX_TOKEN_ENCRYPTION_KEY",
|
||||
base64.urlsafe_b64encode(b"k" * 32).decode().rstrip("="),
|
||||
)
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.main import (
|
||||
BitrixClient,
|
||||
TokenCipher,
|
||||
canonical_fingerprint,
|
||||
normalize_event,
|
||||
resolve_inbound_file_urls,
|
||||
retry_delay,
|
||||
safely_retryable,
|
||||
validate_portal,
|
||||
)
|
||||
|
||||
|
||||
def test_token_cipher_binds_aad():
|
||||
cipher = TokenCipher(os.environ["BITRIX_TOKEN_ENCRYPTION_KEY"], "v1")
|
||||
ciphertext, nonce = cipher.encrypt("secret", "member", "han0107.bitrix24.ru", "access")
|
||||
assert cipher.decrypt(ciphertext, nonce, "member", "han0107.bitrix24.ru", "access") == "secret"
|
||||
with pytest.raises(Exception):
|
||||
cipher.decrypt(ciphertext, nonce, "other", "han0107.bitrix24.ru", "access")
|
||||
|
||||
|
||||
def test_normalize_message_and_finish():
|
||||
external = str(uuid.uuid4())
|
||||
message = normalize_event(
|
||||
{
|
||||
"event": "ONIMCONNECTORMESSAGEADD",
|
||||
"data": {
|
||||
"MESSAGES": [
|
||||
{
|
||||
"im": {"chat_id": 1807, "message_id": 86497},
|
||||
"chat": {"id": external},
|
||||
"message": {"text": "Ответ", "files": []},
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
assert message["event_type"] == "message.new"
|
||||
assert message["external_chat_id"] == external
|
||||
assert message["bitrix_message_id"] == "86497"
|
||||
assert message["message"]["text"] == "Ответ"
|
||||
closed = normalize_event(
|
||||
{"event": "ONIMCONNECTORDIALOGFINISH", "data": {"external_chat_id": external}}
|
||||
)
|
||||
assert closed["event_type"] == "dialog.closed"
|
||||
|
||||
|
||||
def test_normalize_message_removes_bitrix_sender_prefix():
|
||||
external = str(uuid.uuid4())
|
||||
message = normalize_event(
|
||||
{
|
||||
"event": "ONIMCONNECTORMESSAGEADD",
|
||||
"data": {
|
||||
"MESSAGES": [
|
||||
{
|
||||
"im": {"message_id": 86498},
|
||||
"chat": {"id": external},
|
||||
"message": {
|
||||
"text": "[b]Антон Пичугин:[/b] [br]опять ты?",
|
||||
"files": [],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert message["message"]["text"] == "опять ты?"
|
||||
|
||||
|
||||
def test_normalize_bitrix_file_uses_download_url_and_infers_mime_type():
|
||||
external = str(uuid.uuid4())
|
||||
message = normalize_event(
|
||||
{
|
||||
"event": "ONIMCONNECTORMESSAGEADD",
|
||||
"data": {
|
||||
"MESSAGES": [
|
||||
{
|
||||
"im": {"chat_id": 1807, "message_id": 86498},
|
||||
"chat": {"id": external},
|
||||
"message": {
|
||||
"text": "",
|
||||
"files": [
|
||||
{
|
||||
"name": "image.png",
|
||||
"type": "image",
|
||||
"size": 941380,
|
||||
"urlDownload": "https://portal.bitrix24.ru/download/file.png",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert message["message"]["files"] == [
|
||||
{
|
||||
"name": "image.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 941380,
|
||||
"download_url": "https://portal.bitrix24.ru/download/file.png",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_normalize_bitrix_file_uses_open_lines_download_link_and_mime():
|
||||
external = str(uuid.uuid4())
|
||||
message = normalize_event(
|
||||
{
|
||||
"event": "ONIMCONNECTORMESSAGEADD",
|
||||
"data": {
|
||||
"MESSAGES": [
|
||||
{
|
||||
"im": {"chat_id": 1807, "message_id": 86499},
|
||||
"chat": {"id": external},
|
||||
"message": {
|
||||
"text": "",
|
||||
"files": [
|
||||
{
|
||||
"name": "diploma.jpg",
|
||||
"type": "image",
|
||||
"mime": "image/jpeg",
|
||||
"size": 236934,
|
||||
"downloadLink": (
|
||||
"https://portal.bitrix24.ru/download/diploma.jpg"
|
||||
),
|
||||
"link": "https://portal.bitrix24.ru/view/diploma.jpg",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert message["message"]["files"] == [
|
||||
{
|
||||
"name": "diploma.jpg",
|
||||
"mime_type": "image/jpeg",
|
||||
"size_bytes": 236934,
|
||||
"download_url": "https://portal.bitrix24.ru/download/diploma.jpg",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_inbound_file_url_from_bitrix_file_id(monkeypatch):
|
||||
class SessionContext:
|
||||
async def __aenter__(self):
|
||||
return object()
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
return None
|
||||
|
||||
class Bitrix:
|
||||
async def call(self, portal, method, params):
|
||||
assert portal == "portal"
|
||||
assert method == "im.v2.File.download"
|
||||
assert params == {"id": "5155"}
|
||||
return {"downloadUrl": "https://portal.bitrix24.ru/download/file.png"}
|
||||
|
||||
async def fake_active_portal(_session):
|
||||
return "portal"
|
||||
|
||||
monkeypatch.setattr("app.main.active_portal", fake_active_portal)
|
||||
app = SimpleNamespace(
|
||||
state=SimpleNamespace(sessions=lambda: SessionContext(), bitrix=Bitrix())
|
||||
)
|
||||
payload = {
|
||||
"message": {
|
||||
"files": [
|
||||
{
|
||||
"name": "image.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 941380,
|
||||
"download_url": "",
|
||||
"_bitrix_file_id": "5155",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
await resolve_inbound_file_urls(app, payload)
|
||||
|
||||
assert payload["message"]["files"] == [
|
||||
{
|
||||
"name": "image.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 941380,
|
||||
"download_url": "https://portal.bitrix24.ru/download/file.png",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_fingerprint_ignores_signed_query_and_portal_validation():
|
||||
payload = {"message": {"files": [{"download_url": "https://s3/object?sig=one"}]}}
|
||||
other = {"message": {"files": [{"download_url": "https://s3/object?sig=two"}]}}
|
||||
assert canonical_fingerprint(payload) == canonical_fingerprint(other)
|
||||
validate_portal(
|
||||
"han0107.bitrix24.ru",
|
||||
"https://han0107.bitrix24.ru/rest/",
|
||||
"han0107.bitrix24.ru",
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
validate_portal("evil.example", "https://evil.example/rest/", "han0107.bitrix24.ru")
|
||||
assert 0 <= retry_delay(4, 300) <= 8
|
||||
|
||||
|
||||
def test_network_timeouts_are_retryable():
|
||||
assert safely_retryable(TimeoutError("Delivery operation timed out"))
|
||||
assert safely_retryable(httpx.ReadTimeout("Bitrix response timed out"))
|
||||
assert safely_retryable(httpx.ConnectTimeout("Bitrix connection timed out"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bitrix_call_refreshes_and_retries_once_after_401():
|
||||
auth_values: list[str] = []
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
auth = parse_qs(request.content.decode())["auth"][0]
|
||||
auth_values.append(auth)
|
||||
if auth == "old-access":
|
||||
return httpx.Response(401, request=request)
|
||||
return httpx.Response(200, json={"result": {"ok": True}}, request=request)
|
||||
|
||||
class Cipher:
|
||||
@staticmethod
|
||||
def decrypt(ciphertext, *_):
|
||||
return ciphertext
|
||||
|
||||
portal = SimpleNamespace(
|
||||
access_ciphertext="old-access",
|
||||
access_nonce="nonce",
|
||||
member_id="member",
|
||||
domain="han0107.bitrix24.ru",
|
||||
expires_at=None,
|
||||
)
|
||||
settings = SimpleNamespace(bitrix_http_max_concurrency=2)
|
||||
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
|
||||
client = BitrixClient(http, settings, Cipher(), sessions=None)
|
||||
refresh_calls: list[bool] = []
|
||||
|
||||
async def ensure_fresh(_, *, force=False, stale_access_ciphertext=None):
|
||||
refresh_calls.append(force)
|
||||
if force:
|
||||
assert stale_access_ciphertext == "old-access"
|
||||
portal.access_ciphertext = "new-access"
|
||||
|
||||
client.ensure_fresh = ensure_fresh
|
||||
result = await client.call(portal, "imconnector.send.messages", {"MESSAGE": "test"})
|
||||
|
||||
assert result == {"ok": True}
|
||||
assert auth_values == ["old-access", "new-access"]
|
||||
assert refresh_calls == [False, True]
|
||||
Reference in New Issue
Block a user