147 lines
5.4 KiB
Python
147 lines
5.4 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
|
|
|
|
|
|
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}],
|
|
},
|
|
)
|
|
|
|
|
|
@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 |