186 lines
6.4 KiB
Python
186 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from contextlib import asynccontextmanager
|
|
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
|
|
from app.crm import CrmOutcome, CrmResult
|
|
from app.engine import INSERT_CRM_COMMAND, UPDATE_CRM_COMMAND, WorkflowEngine
|
|
from app.repository import Profile
|
|
|
|
|
|
class Result:
|
|
rowcount = 1
|
|
|
|
def __init__(self, *, row=None, scalar=None) -> None:
|
|
self.row = row
|
|
self.scalar = scalar
|
|
|
|
def mappings(self):
|
|
return self
|
|
|
|
def one(self):
|
|
return self.row
|
|
|
|
def scalar_one_or_none(self):
|
|
return self.scalar
|
|
|
|
|
|
class Connection:
|
|
def __init__(self, statements: list[str]) -> None:
|
|
self.statements = statements
|
|
|
|
async def execute(self, statement, params=None):
|
|
sql = str(statement)
|
|
self.statements.append(sql)
|
|
if "INSERT INTO bitrix_sync.business_alerts" in sql:
|
|
now = datetime.now(UTC)
|
|
return Result(
|
|
row={
|
|
"id": uuid.uuid4(),
|
|
"alert_number": 1,
|
|
"fingerprint": "fingerprint",
|
|
"alert_type": params["type"],
|
|
"severity": "warning",
|
|
"app_user_id": params["user_id"],
|
|
"selected_external_id": params["selected_external_id"],
|
|
"candidate_external_ids": params["candidates"],
|
|
"remote_item_id": None,
|
|
"occurrence_count": 1,
|
|
"first_occurred_at": now,
|
|
"last_occurred_at": now,
|
|
}
|
|
)
|
|
if "SELECT value_json FROM bitrix_sync.settings" in sql:
|
|
return Result(
|
|
scalar={
|
|
"entity_type_id": 178,
|
|
"category_id": 5,
|
|
"stage_new": "DT178_5:NEW",
|
|
"field_ids": {"candidate_external_ids": "ufCrm_999"},
|
|
}
|
|
)
|
|
return Result()
|
|
|
|
|
|
class FakeRepository:
|
|
def __init__(self) -> None:
|
|
self.mapping = None
|
|
self.statements: list[str] = []
|
|
|
|
@asynccontextmanager
|
|
async def transaction(self):
|
|
yield Connection(self.statements)
|
|
|
|
async def active_mapping(self, user_id):
|
|
return self.mapping
|
|
|
|
async def reserve_limiter_token(self, refill_per_second, burst):
|
|
return 0
|
|
|
|
|
|
class FakeCrm:
|
|
def __init__(
|
|
self,
|
|
user_id: uuid.UUID,
|
|
*,
|
|
existing_identity: bool = False,
|
|
foreign_owner: bool = False,
|
|
) -> None:
|
|
self.user_id = user_id
|
|
self.existing_identity = existing_identity
|
|
self.foreign_owner = foreign_owner
|
|
self.calls: list[tuple[str, dict]] = []
|
|
|
|
async def call(self, method, params, *, mutating):
|
|
self.calls.append((method, params))
|
|
if method == "crm.duplicate.findbycomm":
|
|
return CrmResult(CrmOutcome.SUCCEEDED, {"CONTACT": ["9", "10"]})
|
|
if method == "crm.contact.get":
|
|
contact_id = str(params["id"])
|
|
return CrmResult(
|
|
CrmOutcome.SUCCEEDED,
|
|
{
|
|
"ID": contact_id,
|
|
"CREATED_TIME": "2026-08-06T10:00:00Z",
|
|
"UF_CRM_100": (
|
|
str(self.user_id)
|
|
if self.existing_identity and contact_id == "10"
|
|
else str(uuid.UUID(int=1))
|
|
if self.foreign_owner and contact_id == "10"
|
|
else None
|
|
),
|
|
},
|
|
)
|
|
if method == "crm.contact.add":
|
|
return CrmResult(CrmOutcome.SUCCEEDED, "11")
|
|
if method == "crm.item.add":
|
|
return CrmResult(CrmOutcome.SUCCEEDED, {"item": {"id": "500"}})
|
|
return CrmResult(CrmOutcome.SUCCEEDED, True)
|
|
|
|
|
|
def test_crm_command_json_payloads_have_explicit_jsonb_types() -> None:
|
|
assert isinstance(INSERT_CRM_COMMAND._bindparams["safe_request"].type, JSONB)
|
|
assert isinstance(UPDATE_CRM_COMMAND._bindparams["safe_response"].type, JSONB)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_contacts_choose_numeric_newest(full_settings) -> None:
|
|
user_id = uuid.uuid4()
|
|
repository = FakeRepository()
|
|
crm = FakeCrm(user_id)
|
|
engine = WorkflowEngine(repository, crm, full_settings)
|
|
await engine._map_or_create(
|
|
uuid.uuid4(),
|
|
Profile(user_id=user_id, phone="+79001234567", identity_status="A", profile_status="A"),
|
|
)
|
|
updates = [params for method, params in crm.calls if method == "crm.contact.update"]
|
|
assert updates[0]["id"] == "10"
|
|
assert not any(method == "crm.contact.add" for method, _ in crm.calls)
|
|
assert any(method == "crm.item.add" for method, _ in crm.calls)
|
|
alert_add = next(params for method, params in crm.calls if method == "crm.item.add")
|
|
assert alert_add["fields"]["contactIds"] == [9, 10]
|
|
assert alert_add["fields"]["sourceId"] == "WEB"
|
|
assert "ufCrm_999" not in alert_add["fields"]
|
|
assert any("entity_external_mapping" in statement for statement in repository.statements)
|
|
assert not any("digest(" in statement for statement in repository.statements)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recovered_duplicate_creates_alert_before_mapping(full_settings) -> None:
|
|
user_id = uuid.uuid4()
|
|
repository = FakeRepository()
|
|
crm = FakeCrm(user_id, existing_identity=True)
|
|
engine = WorkflowEngine(repository, crm, full_settings)
|
|
|
|
await engine._map_or_create(
|
|
uuid.uuid4(),
|
|
Profile(user_id=user_id, phone="+79001234567", identity_status="A", profile_status="A"),
|
|
)
|
|
|
|
methods = [method for method, _ in crm.calls]
|
|
assert "crm.item.add" in methods
|
|
assert "crm.contact.update" not in methods
|
|
assert any("entity_external_mapping" in statement for statement in repository.statements)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_foreign_owned_contact_alert_includes_new_contact(full_settings) -> None:
|
|
user_id = uuid.uuid4()
|
|
repository = FakeRepository()
|
|
crm = FakeCrm(user_id, foreign_owner=True)
|
|
engine = WorkflowEngine(repository, crm, full_settings)
|
|
|
|
await engine._map_or_create(
|
|
uuid.uuid4(),
|
|
Profile(user_id=user_id, phone="+79001234567", identity_status="A", profile_status="A"),
|
|
)
|
|
|
|
methods = [method for method, _ in crm.calls]
|
|
alert_add = next(params for method, params in crm.calls if method == "crm.item.add")
|
|
assert methods.index("crm.contact.add") < methods.index("crm.item.add")
|
|
assert alert_add["fields"]["contactIds"] == [9, 10, 11]
|