Files
ecm_sync_system/tests/unit/test_project_detail_sync_strategy.py
T
2026-04-02 11:14:44 +08:00

222 lines
8.0 KiB
Python

from types import SimpleNamespace
from typing import cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import pytest_asyncio
import sync_state_machine.domain # noqa: F401
from sync_state_machine.common.binding import BindingManager
from sync_state_machine.common.collection import DataCollection
from sync_state_machine.common.sync_node import SyncNode
from sync_state_machine.common.sqlite_backend import SQLitePersistenceBackend
from sync_state_machine.common.types import BindingStatus
from sync_state_machine.domain.project_detail.sync_strategy import ProjectDetailSyncStrategy
from sync_state_machine.domain.project_detail.sync_node import ProjectDetailSyncNode
from sync_state_machine.sync_system.config import UpdateDirection
from sync_state_machine.sync_system.strategy_ops import BoundNodePair
from pydantic import BaseModel
def _fake_node(*, node_id: str, key: str):
data = {"id": node_id, "key": key}
return cast(
SyncNode,
SimpleNamespace(
node_id=node_id,
data_id=node_id,
action=None,
context={},
get_data=lambda: data,
),
)
def _build_project_detail_node(*, node_id: str, data_id: str, key: str) -> ProjectDetailSyncNode:
return ProjectDetailSyncNode(
node_id=node_id,
data_id=data_id,
data={
"id": data_id,
"project_id": "project-1",
"key": key,
"value": [{"year": 2026, "capacity": 4.0}],
},
)
class _ProjectSchema(BaseModel):
id: str
plan_production_date: str | None = None
plan_full_production_date: str | None = None
actual_production_date: str | None = None
actual_full_production_date: str | None = None
class _ProjectNode(SyncNode[_ProjectSchema]):
node_type = "project"
schema = _ProjectSchema
@pytest_asyncio.fixture
async def project_detail_env(tmp_path):
persistence = SQLitePersistenceBackend(str(tmp_path / "project-detail.db"))
await persistence.initialize()
local = DataCollection("local", persistence=persistence, auto_persist=False)
remote = DataCollection("remote", persistence=persistence, auto_persist=False)
binding_manager = BindingManager(persistence, auto_persist=False)
yield local, remote, binding_manager, persistence
await persistence.close()
@pytest.mark.asyncio
async def test_project_detail_process_update_pairs_partitions_group_production_plans_before_generic_logic():
strategy = ProjectDetailSyncStrategy("project_detail_data", MagicMock(), MagicMock(), MagicMock())
pull_pair = BoundNodePair(
local_node=_fake_node(node_id="local-pull", key="ensure_production"),
remote_node=_fake_node(node_id="remote-pull", key="ensure_production"),
)
generic_pair = BoundNodePair(
local_node=_fake_node(node_id="local-generic", key="production"),
remote_node=_fake_node(node_id="remote-generic", key="production"),
)
pull_node = SimpleNamespace(node_id="pull-update")
generic_node = SimpleNamespace(node_id="generic-update")
with patch.object(
strategy,
"prepare_update_for_direction",
new=AsyncMock(return_value=pull_node),
) as mock_prepare, patch.object(
strategy,
"update_pair",
new=AsyncMock(return_value=[generic_node]),
) as mock_update_pair:
updated_by_id = await strategy.process_update_pairs([pull_pair, generic_pair], {})
assert updated_by_id == {
"pull-update": pull_node,
"generic-update": generic_node,
}
mock_prepare.assert_awaited_once_with(
pull_pair.local_node,
pull_pair.remote_node,
UpdateDirection.PULL,
{},
)
mock_update_pair.assert_awaited_once_with(generic_pair.local_node, generic_pair.remote_node, {})
def test_project_detail_domain_option_is_typed_schema() -> None:
strategy = ProjectDetailSyncStrategy("project_detail_data", MagicMock(), MagicMock(), MagicMock())
assert strategy.domain_option.pull_group_production_plans is True
@pytest.mark.asyncio
async def test_project_detail_bind_create_pulls_group_production_plan_from_remote(project_detail_env) -> None:
local, remote, binding_manager, _ = project_detail_env
strategy = ProjectDetailSyncStrategy("project_detail_data", local, remote, binding_manager)
strategy.config.depend_fields = {}
remote_node = _build_project_detail_node(
node_id="remote-pull",
data_id="detail-remote-pull",
key="ensure_production",
)
await remote.add(remote_node)
await strategy.bind()
created_nodes = await strategy.create()
assert len(created_nodes) == 1
created_node = created_nodes[0]
created_data = created_node.get_data()
assert created_node.node_type == "project_detail_data"
assert created_data is not None
assert created_data["key"] == "ensure_production"
assert remote_node.binding_status == BindingStatus.PEER_CREATING
assert await binding_manager.get_local_id("project_detail_data", remote_node.node_id) == created_node.node_id
@pytest.mark.asyncio
async def test_project_detail_bind_does_not_push_group_production_plan_from_local(project_detail_env) -> None:
local, remote, binding_manager, _ = project_detail_env
strategy = ProjectDetailSyncStrategy("project_detail_data", local, remote, binding_manager)
strategy.config.depend_fields = {}
local_node = _build_project_detail_node(
node_id="local-pull",
data_id="detail-local-pull",
key="ensure_production",
)
await local.add(local_node)
await strategy.bind()
created_nodes = await strategy.create()
assert created_nodes == []
assert remote.filter(node_type="project_detail_data") == []
assert await binding_manager.get_remote_id("project_detail_data", local_node.node_id) is None
@pytest.mark.asyncio
async def test_project_detail_create_remote_production_carries_project_plan_dates(project_detail_env) -> None:
local, remote, binding_manager, _ = project_detail_env
strategy = ProjectDetailSyncStrategy("project_detail_data", local, remote, binding_manager)
local_project = _ProjectNode(
node_id="local-project-node",
data_id="local-project-id",
data={
"id": "local-project-id",
"plan_production_date": "2023-12-30",
"plan_full_production_date": "2024-12-30",
"actual_production_date": "2023-12-30",
"actual_full_production_date": "2024-12-30",
},
)
remote_project = _ProjectNode(
node_id="remote-project-node",
data_id="remote-project-id",
data={"id": "remote-project-id"},
)
await local.add(local_project)
await remote.add(remote_project)
await binding_manager.bind("project", local_project.node_id, remote_project.node_id)
local_detail = ProjectDetailSyncNode(
node_id="local-detail-node",
data_id="local-detail-id",
data={
"id": "local-detail-id",
"project_id": "local-project-id",
"key": "production",
"value": [{"date": "2023-12-30", "capacity": 3.0}],
},
)
await local.add(local_detail)
created_remote = ProjectDetailSyncNode(
node_id="remote-detail-node",
data_id="",
data={
"id": "",
"project_id": "remote-project-id",
"key": "production",
"value": [{"date": "2023-12-30", "capacity": 3.0}],
},
)
await remote.add(created_remote)
await binding_manager.bind("project_detail_data", local_detail.node_id, created_remote.node_id)
await strategy._enrich_created_production_nodes([created_remote], source_is_local=True)
created_data = created_remote.get_data()
assert created_data is not None
assert created_data["project_id"] == "remote-project-id"
assert created_data["plan_production_date"] == "2023-12-30"
assert created_data["plan_full_production_date"] == "2024-12-30"
assert created_data["actual_production_date"] == "2023-12-30"
assert created_data["actual_full_production_date"] == "2024-12-30"