Финальная стабильная реализация
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.repository import Repository
|
||||
from app.domain import safe_hash
|
||||
from app.reconciliation import IncrementalReconciler
|
||||
from app.repository import LeasedTask, Repository
|
||||
|
||||
|
||||
class FakeResult:
|
||||
@@ -16,9 +20,15 @@ class FakeResult:
|
||||
def __iter__(self):
|
||||
return iter(self.rows)
|
||||
|
||||
def mappings(self):
|
||||
return self
|
||||
|
||||
def scalar_one_or_none(self):
|
||||
return self.scalar
|
||||
|
||||
def scalar_one(self):
|
||||
return self.scalar
|
||||
|
||||
|
||||
class FakeConnection:
|
||||
async def execute(self, statement):
|
||||
@@ -38,10 +48,29 @@ class FakeConnection:
|
||||
|
||||
|
||||
class FakeEngine:
|
||||
def __init__(self) -> None:
|
||||
self.queries: list[str] = []
|
||||
|
||||
@asynccontextmanager
|
||||
async def connect(self):
|
||||
yield FakeConnection()
|
||||
|
||||
@asynccontextmanager
|
||||
async def begin(self):
|
||||
yield ClaimConnection(self.queries)
|
||||
|
||||
|
||||
class ClaimConnection:
|
||||
def __init__(self, queries: list[str]) -> None:
|
||||
self.queries = queries
|
||||
|
||||
async def execute(self, statement, params):
|
||||
sql = str(statement)
|
||||
self.queries.append(sql)
|
||||
if "INSERT INTO bitrix_sync.workflow_instances" in sql:
|
||||
return FakeResult(scalar=params["id"])
|
||||
return FakeResult()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_uses_common_status_alias_for_workflows() -> None:
|
||||
@@ -55,3 +84,95 @@ async def test_status_uses_common_status_alias_for_workflows() -> None:
|
||||
assert result["commands"] == {"succeeded": 4}
|
||||
assert result["webhook_lag_seconds"] == 1.5
|
||||
assert result["settings_version"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_claims_recover_expired_leases() -> None:
|
||||
repository = object.__new__(Repository)
|
||||
engine = FakeEngine()
|
||||
repository.engine = engine
|
||||
|
||||
assert await repository.claim_tasks("worker", 10, 60) == []
|
||||
assert await repository.claim_webhooks("worker", 10, 60) == []
|
||||
|
||||
assert "status='leased' AND locked_until < now()" in engine.queries[0]
|
||||
assert "status='processing' AND locked_until<now()" in engine.queries[1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_workflow_marks_recovered_workflow_running() -> None:
|
||||
repository = object.__new__(Repository)
|
||||
engine = FakeEngine()
|
||||
repository.engine = engine
|
||||
task = LeasedTask(uuid.uuid4(), "contact.map_or_create", uuid.uuid4(), uuid.uuid4(), 0)
|
||||
|
||||
workflow_id = await repository.create_workflow(task)
|
||||
|
||||
assert isinstance(workflow_id, uuid.UUID)
|
||||
assert "SET state='running'" in engine.queries[1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconciliation_uses_unambiguous_external_id_alias() -> None:
|
||||
queries: list[str] = []
|
||||
|
||||
class Connection:
|
||||
async def execute(self, statement, params):
|
||||
queries.append(str(statement))
|
||||
return FakeResult()
|
||||
|
||||
class ReconciliationRepository:
|
||||
@asynccontextmanager
|
||||
async def transaction(self):
|
||||
yield Connection()
|
||||
|
||||
reconciler = IncrementalReconciler(ReconciliationRepository(), None, "ufCrm_1")
|
||||
await reconciler._enqueue_changed(
|
||||
[
|
||||
("29406", "2026-08-20T13:21:00+00:00"),
|
||||
("29502", "2026-08-20T13:22:00+00:00"),
|
||||
]
|
||||
)
|
||||
|
||||
assert "AS candidate(external_id,event_id)" in queries[0]
|
||||
assert "w.external_entity_id=candidate.external_id" in queries[0]
|
||||
assert "ON CONFLICT (receiver_type,event_id)" in queries[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_crm_profile_hashes_without_database_digest() -> None:
|
||||
calls: list[tuple[str, dict | None]] = []
|
||||
|
||||
class Connection:
|
||||
async def execute(self, statement, params=None):
|
||||
calls.append((str(statement), params))
|
||||
return FakeResult()
|
||||
|
||||
class Engine:
|
||||
@asynccontextmanager
|
||||
async def begin(self):
|
||||
yield Connection()
|
||||
|
||||
repository = object.__new__(Repository)
|
||||
repository.engine = Engine()
|
||||
source_updated_at = datetime.now(UTC)
|
||||
|
||||
await repository.apply_crm_profile(
|
||||
uuid.uuid4(),
|
||||
"29406",
|
||||
full_name="Тестовый пользователь",
|
||||
citizenship=None,
|
||||
email=None,
|
||||
source_updated_at=source_updated_at,
|
||||
source="webhook",
|
||||
)
|
||||
|
||||
profile_sql, profile_params = calls[1]
|
||||
snapshot_sql, snapshot_params = calls[2]
|
||||
assert "source_updated_at=:source_updated_at" in profile_sql
|
||||
assert profile_params["source_updated_at"] == source_updated_at
|
||||
assert "digest(" not in snapshot_sql
|
||||
assert "CAST(:external_id AS varchar(128))" in snapshot_sql
|
||||
assert "CAST(:source AS varchar(24))" in snapshot_sql
|
||||
assert snapshot_params["full_name_hash"] == safe_hash("Тестовый пользователь")
|
||||
assert snapshot_params["email_hash"] == safe_hash("")
|
||||
|
||||
Reference in New Issue
Block a user