459 lines
19 KiB
Python
459 lines
19 KiB
Python
"""Tests for IntegrationConverter and SyncRunConverter infrastructure conversions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from datetime import UTC, datetime
|
|
from unittest.mock import MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from noteflow.domain.entities.integration import (
|
|
Integration,
|
|
IntegrationStatus,
|
|
IntegrationType,
|
|
SyncErrorCode,
|
|
SyncRun,
|
|
SyncRunStatus,
|
|
)
|
|
from noteflow.infrastructure.converters.integration_converters import (
|
|
IntegrationConverter,
|
|
SyncRunConverter,
|
|
)
|
|
|
|
|
|
def _create_mock_integration_orm_from_kwargs(orm_kwargs: Mapping[str, object]) -> MagicMock:
|
|
"""Create a mock ORM model from Integration kwargs dictionary."""
|
|
mock_orm = MagicMock()
|
|
mock_orm.id = orm_kwargs["id"]
|
|
mock_orm.workspace_id = orm_kwargs["workspace_id"]
|
|
mock_orm.name = orm_kwargs["name"]
|
|
mock_orm.type = orm_kwargs["type"]
|
|
mock_orm.status = orm_kwargs["status"]
|
|
mock_orm.config = orm_kwargs["config"]
|
|
mock_orm.last_sync = orm_kwargs["last_sync"]
|
|
mock_orm.error_message = orm_kwargs["error_message"]
|
|
mock_orm.created_at = orm_kwargs["created_at"]
|
|
mock_orm.updated_at = orm_kwargs["updated_at"]
|
|
return mock_orm
|
|
|
|
|
|
def _create_mock_sync_run_orm_from_kwargs(orm_kwargs: Mapping[str, object]) -> MagicMock:
|
|
"""Create a mock ORM model from SyncRun kwargs dictionary."""
|
|
mock_orm = MagicMock()
|
|
mock_orm.id = orm_kwargs["id"]
|
|
mock_orm.integration_id = orm_kwargs["integration_id"]
|
|
mock_orm.status = orm_kwargs["status"]
|
|
mock_orm.started_at = orm_kwargs["started_at"]
|
|
mock_orm.ended_at = orm_kwargs["ended_at"]
|
|
mock_orm.duration_ms = orm_kwargs["duration_ms"]
|
|
mock_orm.error_code = orm_kwargs["error_code"]
|
|
mock_orm.stats = orm_kwargs["stats"]
|
|
return mock_orm
|
|
|
|
|
|
# Test constants for sync run metrics
|
|
SYNC_RUN_ITEMS_SYNCED = 15
|
|
"""Number of items synced in a standard test sync run fixture."""
|
|
|
|
SYNC_RUN_DURATION_MS_SHORT = 5000
|
|
"""Short sync run duration in milliseconds (5 seconds)."""
|
|
|
|
SYNC_RUN_DURATION_MS_MEDIUM = 10000
|
|
"""Medium sync run duration in milliseconds (10 seconds)."""
|
|
|
|
SYNC_RUN_ITEMS_COMPLETE = 25
|
|
"""Number of items in a complete sync run test case."""
|
|
|
|
|
|
class TestIntegrationConverterOrmToDomain:
|
|
"""Tests for IntegrationConverter.orm_to_domain."""
|
|
|
|
@pytest.fixture
|
|
def mock_integration_model(self) -> MagicMock:
|
|
"""Create mock ORM IntegrationModel with typical values."""
|
|
model = MagicMock()
|
|
model.id = uuid4()
|
|
model.workspace_id = uuid4()
|
|
model.name = "Google Calendar"
|
|
model.type = "calendar"
|
|
model.status = "connected"
|
|
model.config = {"provider_email": "user@example.com", "refresh_token": "xyz"}
|
|
model.last_sync = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
|
|
model.error_message = None
|
|
model.created_at = datetime(2024, 1, 10, 10, 0, 0, tzinfo=UTC)
|
|
model.updated_at = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
|
|
return model
|
|
|
|
def test_integration_orm_to_domain(self, mock_integration_model: MagicMock) -> None:
|
|
"""Convert ORM model to domain Integration."""
|
|
result = IntegrationConverter.orm_to_domain(mock_integration_model)
|
|
|
|
assert isinstance(result, Integration), "Should return Integration instance"
|
|
assert result.id == mock_integration_model.id, "ID should match"
|
|
assert result.workspace_id == mock_integration_model.workspace_id, (
|
|
"Workspace ID should match"
|
|
)
|
|
assert result.name == "Google Calendar", "Name should match"
|
|
assert result.type == IntegrationType.CALENDAR, "Type should be enum"
|
|
assert result.status == IntegrationStatus.CONNECTED, "Status should be enum"
|
|
assert result.last_sync == mock_integration_model.last_sync, "Last sync should match"
|
|
assert result.error_message is None, "Error message should be None"
|
|
|
|
def test_converts_type_string_to_enum(self, mock_integration_model: MagicMock) -> None:
|
|
"""Type string is converted to IntegrationType enum."""
|
|
mock_integration_model.type = "email"
|
|
result = IntegrationConverter.orm_to_domain(mock_integration_model)
|
|
assert result.type == IntegrationType.EMAIL, "Type should be EMAIL enum"
|
|
assert isinstance(result.type, IntegrationType), "Type should be IntegrationType instance"
|
|
|
|
@pytest.mark.parametrize(
|
|
("type_string", "expected_enum"),
|
|
[
|
|
("auth", IntegrationType.AUTH),
|
|
("email", IntegrationType.EMAIL),
|
|
("calendar", IntegrationType.CALENDAR),
|
|
("pkm", IntegrationType.PKM),
|
|
("custom", IntegrationType.CUSTOM),
|
|
],
|
|
)
|
|
def test_all_type_strings_convert_correctly(
|
|
self,
|
|
mock_integration_model: MagicMock,
|
|
type_string: str,
|
|
expected_enum: IntegrationType,
|
|
) -> None:
|
|
"""All IntegrationType strings convert to correct enum values."""
|
|
mock_integration_model.type = type_string
|
|
result = IntegrationConverter.orm_to_domain(mock_integration_model)
|
|
assert result.type == expected_enum, (
|
|
f"Type string '{type_string}' should convert to {expected_enum}"
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
("status_string", "expected_enum"),
|
|
[
|
|
("disconnected", IntegrationStatus.DISCONNECTED),
|
|
("connected", IntegrationStatus.CONNECTED),
|
|
("error", IntegrationStatus.ERROR),
|
|
],
|
|
)
|
|
def test_integration_status_strings_convert(
|
|
self,
|
|
mock_integration_model: MagicMock,
|
|
status_string: str,
|
|
expected_enum: IntegrationStatus,
|
|
) -> None:
|
|
"""All IntegrationStatus strings convert to correct enum values."""
|
|
mock_integration_model.status = status_string
|
|
result = IntegrationConverter.orm_to_domain(mock_integration_model)
|
|
assert result.status == expected_enum, (
|
|
f"Status string '{status_string}' should convert to {expected_enum}"
|
|
)
|
|
|
|
def test_converts_config_dict(self, mock_integration_model: MagicMock) -> None:
|
|
"""Config is converted to dict from ORM model."""
|
|
result = IntegrationConverter.orm_to_domain(mock_integration_model)
|
|
assert isinstance(result.config, dict), "Config should be dict"
|
|
assert result.config["provider_email"] == "user@example.com", (
|
|
"Provider email should be preserved"
|
|
)
|
|
|
|
def test_handles_none_config(self, mock_integration_model: MagicMock) -> None:
|
|
"""None config in ORM becomes empty dict in domain."""
|
|
mock_integration_model.config = None
|
|
result = IntegrationConverter.orm_to_domain(mock_integration_model)
|
|
assert result.config == {}, "None config should become empty dict"
|
|
|
|
def test_integration_handles_error_status(self, mock_integration_model: MagicMock) -> None:
|
|
"""Error status with error message is converted correctly."""
|
|
mock_integration_model.status = "error"
|
|
mock_integration_model.error_message = "Token expired"
|
|
|
|
result = IntegrationConverter.orm_to_domain(mock_integration_model)
|
|
|
|
assert result.status == IntegrationStatus.ERROR, "Status should be ERROR enum"
|
|
assert result.error_message == "Token expired", "Error message should be preserved"
|
|
|
|
|
|
class TestIntegrationConverterToOrmKwargs:
|
|
"""Tests for IntegrationConverter.to_integration_orm_kwargs."""
|
|
|
|
@pytest.fixture
|
|
def integration_entity(self) -> Integration:
|
|
"""Create domain Integration entity for testing."""
|
|
return Integration(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Outlook Calendar",
|
|
type=IntegrationType.CALENDAR,
|
|
status=IntegrationStatus.CONNECTED,
|
|
config={"provider_email": "test@outlook.com"},
|
|
last_sync=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
|
|
error_message=None,
|
|
created_at=datetime(2024, 1, 10, 10, 0, 0, tzinfo=UTC),
|
|
updated_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
|
|
)
|
|
|
|
def test_integration_to_orm_kwargs(self, integration_entity: Integration) -> None:
|
|
"""Convert domain Integration to ORM kwargs dict."""
|
|
result = IntegrationConverter.to_integration_orm_kwargs(integration_entity)
|
|
|
|
assert result["id"] == integration_entity.id, "ID should be preserved"
|
|
assert result["workspace_id"] == integration_entity.workspace_id, (
|
|
"Workspace ID should be preserved"
|
|
)
|
|
assert result["name"] == "Outlook Calendar", "Name should be preserved"
|
|
assert result["type"] == "calendar", "Type should be string value"
|
|
assert result["status"] == "connected", "Status should be string value"
|
|
assert result["config"] == {"provider_email": "test@outlook.com"}, (
|
|
"Config should be preserved"
|
|
)
|
|
assert result["error_message"] is None, "Error message should be None"
|
|
|
|
@pytest.mark.parametrize(
|
|
("type_enum", "expected_string"),
|
|
[
|
|
(IntegrationType.AUTH, "auth"),
|
|
(IntegrationType.EMAIL, "email"),
|
|
(IntegrationType.CALENDAR, "calendar"),
|
|
(IntegrationType.PKM, "pkm"),
|
|
(IntegrationType.CUSTOM, "custom"),
|
|
],
|
|
)
|
|
def test_all_type_enums_convert_to_strings(
|
|
self, type_enum: IntegrationType, expected_string: str
|
|
) -> None:
|
|
"""All IntegrationType enums convert to correct string values."""
|
|
integration = Integration(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Test",
|
|
type=type_enum,
|
|
status=IntegrationStatus.DISCONNECTED,
|
|
)
|
|
result = IntegrationConverter.to_integration_orm_kwargs(integration)
|
|
assert result["type"] == expected_string, (
|
|
f"Type enum {type_enum} should convert to '{expected_string}'"
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
("status_enum", "expected_string"),
|
|
[
|
|
(IntegrationStatus.DISCONNECTED, "disconnected"),
|
|
(IntegrationStatus.CONNECTED, "connected"),
|
|
(IntegrationStatus.ERROR, "error"),
|
|
],
|
|
)
|
|
def test_integration_status_enums_to_strings(
|
|
self, status_enum: IntegrationStatus, expected_string: str
|
|
) -> None:
|
|
"""All IntegrationStatus enums convert to correct string values."""
|
|
integration = Integration(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Test",
|
|
type=IntegrationType.CALENDAR,
|
|
status=status_enum,
|
|
)
|
|
result = IntegrationConverter.to_integration_orm_kwargs(integration)
|
|
assert result["status"] == expected_string, (
|
|
f"Status enum {status_enum} should convert to '{expected_string}'"
|
|
)
|
|
|
|
|
|
class TestSyncRunConverterOrmToDomain:
|
|
"""Tests for SyncRunConverter.orm_to_domain."""
|
|
|
|
@pytest.fixture
|
|
def mock_sync_run_model(self) -> MagicMock:
|
|
"""Create mock ORM IntegrationSyncRunModel with typical values."""
|
|
model = MagicMock()
|
|
model.id = uuid4()
|
|
model.integration_id = uuid4()
|
|
model.status = "success"
|
|
model.started_at = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
|
|
model.ended_at = datetime(2024, 1, 15, 12, 0, 5, tzinfo=UTC)
|
|
model.duration_ms = SYNC_RUN_DURATION_MS_SHORT
|
|
model.error_code = None
|
|
model.stats = {"items_synced": 10, "items_total": SYNC_RUN_ITEMS_SYNCED}
|
|
return model
|
|
|
|
def test_sync_run_orm_to_domain(self, mock_sync_run_model: MagicMock) -> None:
|
|
"""Convert ORM model to domain SyncRun."""
|
|
result = SyncRunConverter.orm_to_domain(mock_sync_run_model)
|
|
|
|
assert isinstance(result, SyncRun), "Should return SyncRun instance"
|
|
assert result.id == mock_sync_run_model.id, "ID should match"
|
|
assert result.integration_id == mock_sync_run_model.integration_id, (
|
|
"Integration ID should match"
|
|
)
|
|
assert result.status == SyncRunStatus.SUCCESS, "Status should be enum"
|
|
assert result.duration_ms == SYNC_RUN_DURATION_MS_SHORT, "Duration should match"
|
|
assert result.error_code is None, "Error code should be None"
|
|
|
|
@pytest.mark.parametrize(
|
|
("status_string", "expected_enum"),
|
|
[
|
|
("running", SyncRunStatus.RUNNING),
|
|
("success", SyncRunStatus.SUCCESS),
|
|
("error", SyncRunStatus.ERROR),
|
|
],
|
|
)
|
|
def test_sync_run_status_strings_convert(
|
|
self,
|
|
mock_sync_run_model: MagicMock,
|
|
status_string: str,
|
|
expected_enum: SyncRunStatus,
|
|
) -> None:
|
|
"""All SyncRunStatus strings convert to correct enum values."""
|
|
mock_sync_run_model.status = status_string
|
|
result = SyncRunConverter.orm_to_domain(mock_sync_run_model)
|
|
assert result.status == expected_enum, (
|
|
f"Status string '{status_string}' should convert to {expected_enum}"
|
|
)
|
|
|
|
def test_converts_stats_dict(self, mock_sync_run_model: MagicMock) -> None:
|
|
"""Stats is converted to dict from ORM model."""
|
|
result = SyncRunConverter.orm_to_domain(mock_sync_run_model)
|
|
assert isinstance(result.stats, dict), "Stats should be dict"
|
|
assert result.stats["items_synced"] == 10, "Items synced count should be preserved"
|
|
assert result.stats["items_total"] == SYNC_RUN_ITEMS_SYNCED, (
|
|
"Items total count should be preserved"
|
|
)
|
|
|
|
def test_handles_none_stats(self, mock_sync_run_model: MagicMock) -> None:
|
|
"""None stats in ORM becomes empty dict in domain."""
|
|
mock_sync_run_model.stats = None
|
|
result = SyncRunConverter.orm_to_domain(mock_sync_run_model)
|
|
assert result.stats == {}, "None stats should become empty dict"
|
|
|
|
def test_sync_run_handles_error_status(self, mock_sync_run_model: MagicMock) -> None:
|
|
"""Error status with error code is converted correctly."""
|
|
mock_sync_run_model.status = "error"
|
|
mock_sync_run_model.error_code = "provider_error"
|
|
|
|
result = SyncRunConverter.orm_to_domain(mock_sync_run_model)
|
|
|
|
assert result.status == SyncRunStatus.ERROR, "Status should be ERROR enum"
|
|
assert result.error_code == SyncErrorCode.PROVIDER_ERROR, "Error code should be preserved"
|
|
|
|
|
|
class TestSyncRunConverterToOrmKwargs:
|
|
"""Tests for SyncRunConverter.to_sync_run_orm_kwargs."""
|
|
|
|
def test_sync_run_to_orm_kwargs(self) -> None:
|
|
"""Convert domain SyncRun to ORM kwargs dict."""
|
|
sync_run = SyncRun(
|
|
id=uuid4(),
|
|
integration_id=uuid4(),
|
|
status=SyncRunStatus.SUCCESS,
|
|
started_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
|
|
ended_at=datetime(2024, 1, 15, 12, 0, 10, tzinfo=UTC),
|
|
duration_ms=SYNC_RUN_DURATION_MS_MEDIUM,
|
|
error_code=None,
|
|
stats={"items_synced": SYNC_RUN_ITEMS_COMPLETE},
|
|
)
|
|
|
|
result = SyncRunConverter.to_sync_run_orm_kwargs(sync_run)
|
|
|
|
assert result["id"] == sync_run.id, "ID should be preserved"
|
|
assert result["integration_id"] == sync_run.integration_id, (
|
|
"Integration ID should be preserved"
|
|
)
|
|
assert result["status"] == "success", "Status should be string value"
|
|
assert result["duration_ms"] == SYNC_RUN_DURATION_MS_MEDIUM, "Duration should be preserved"
|
|
assert result["stats"] == {"items_synced": SYNC_RUN_ITEMS_COMPLETE}, (
|
|
"Stats should be preserved"
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
("status_enum", "expected_string"),
|
|
[
|
|
(SyncRunStatus.RUNNING, "running"),
|
|
(SyncRunStatus.SUCCESS, "success"),
|
|
(SyncRunStatus.ERROR, "error"),
|
|
],
|
|
)
|
|
def test_sync_run_status_enums_to_strings(
|
|
self, status_enum: SyncRunStatus, expected_string: str
|
|
) -> None:
|
|
"""All SyncRunStatus enums convert to correct string values."""
|
|
sync_run = SyncRun(
|
|
id=uuid4(),
|
|
integration_id=uuid4(),
|
|
status=status_enum,
|
|
started_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
|
|
)
|
|
result = SyncRunConverter.to_sync_run_orm_kwargs(sync_run)
|
|
assert result["status"] == expected_string, (
|
|
f"Status enum {status_enum} should convert to '{expected_string}'"
|
|
)
|
|
|
|
|
|
class TestIntegrationConverterRoundTrip:
|
|
"""Tests for round-trip conversion fidelity."""
|
|
|
|
@pytest.fixture
|
|
def round_trip_integration(self) -> Integration:
|
|
"""Create Integration entity for round-trip testing."""
|
|
return Integration(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Round Trip Integration",
|
|
type=IntegrationType.CALENDAR,
|
|
status=IntegrationStatus.CONNECTED,
|
|
config={"provider_email": "test@example.com", "scope": "calendar.read"},
|
|
last_sync=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
|
|
error_message=None,
|
|
created_at=datetime(2024, 1, 10, 10, 0, 0, tzinfo=UTC),
|
|
updated_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
|
|
)
|
|
|
|
@pytest.fixture
|
|
def round_trip_sync_run(self) -> SyncRun:
|
|
"""Create SyncRun entity for round-trip testing."""
|
|
return SyncRun(
|
|
id=uuid4(),
|
|
integration_id=uuid4(),
|
|
status=SyncRunStatus.SUCCESS,
|
|
started_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
|
|
ended_at=datetime(2024, 1, 15, 12, 0, 15, tzinfo=UTC),
|
|
duration_ms=15000,
|
|
error_code=None,
|
|
stats={"items_synced": 50, "items_total": 50},
|
|
)
|
|
|
|
def test_integration_domain_to_orm_to_domain_preserves_values(
|
|
self, round_trip_integration: Integration
|
|
) -> None:
|
|
"""Round-trip conversion preserves all Integration field values."""
|
|
orm_kwargs = IntegrationConverter.to_integration_orm_kwargs(round_trip_integration)
|
|
mock_orm = _create_mock_integration_orm_from_kwargs(orm_kwargs)
|
|
result = IntegrationConverter.orm_to_domain(mock_orm)
|
|
|
|
assert result.id == round_trip_integration.id, "ID preserved through round-trip"
|
|
assert result.workspace_id == round_trip_integration.workspace_id, "Workspace ID preserved"
|
|
assert result.name == round_trip_integration.name, "Name preserved"
|
|
assert result.type == round_trip_integration.type, "Type preserved"
|
|
assert result.status == round_trip_integration.status, "Status preserved"
|
|
assert result.config == round_trip_integration.config, "Config preserved"
|
|
assert result.last_sync == round_trip_integration.last_sync, "Last sync preserved"
|
|
|
|
def test_sync_run_domain_to_orm_to_domain_preserves_values(
|
|
self, round_trip_sync_run: SyncRun
|
|
) -> None:
|
|
"""Round-trip conversion preserves all SyncRun field values."""
|
|
orm_kwargs = SyncRunConverter.to_sync_run_orm_kwargs(round_trip_sync_run)
|
|
mock_orm = _create_mock_sync_run_orm_from_kwargs(orm_kwargs)
|
|
result = SyncRunConverter.orm_to_domain(mock_orm)
|
|
|
|
assert result.id == round_trip_sync_run.id, "ID preserved"
|
|
assert result.integration_id == round_trip_sync_run.integration_id, (
|
|
"Integration ID preserved"
|
|
)
|
|
assert result.status == round_trip_sync_run.status, "Status preserved"
|
|
assert result.duration_ms == round_trip_sync_run.duration_ms, "Duration preserved"
|
|
assert result.stats == round_trip_sync_run.stats, "Stats preserved"
|