179 lines
5.2 KiB
Python
179 lines
5.2 KiB
Python
"""End-to-end tests for the Redmine -> CRM sync pipeline."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
import responses
|
|
|
|
import contact_enrichment
|
|
import sync_redmine_contacts
|
|
from redmine_crm.crm_client import CrmClient
|
|
from redmine_crm.redmine_client import RedmineClient
|
|
|
|
BASE_URL = "https://redmine.example.com"
|
|
|
|
|
|
@responses.activate
|
|
def test_pipeline_full_run_creates_contact():
|
|
contacts = [
|
|
{
|
|
"id": 1,
|
|
"first_name": "Grace",
|
|
"last_name": "Hopper",
|
|
"email": "grace@example.com",
|
|
"updated_at": "2026-07-04T12:00:00Z",
|
|
}
|
|
]
|
|
responses.add(
|
|
responses.GET,
|
|
f"{BASE_URL}/contacts.json",
|
|
json={"contacts": contacts, "total_count": 1},
|
|
status=200,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
state_file = Path(tmpdir) / "state.json"
|
|
redmine = RedmineClient(BASE_URL, "secret", project_id=1)
|
|
crm = CrmClient()
|
|
crm._store.clear()
|
|
state = sync_redmine_contacts.FileSyncState(state_file)
|
|
config = contact_enrichment.EnrichmentConfig()
|
|
|
|
result = sync_redmine_contacts.run_pipeline(
|
|
redmine=redmine,
|
|
crm=crm,
|
|
enrichment_config=config,
|
|
state=state,
|
|
batch_size=100,
|
|
dry_run=False,
|
|
)
|
|
|
|
assert result["metrics"]["fetched"] == 1
|
|
assert result["metrics"]["enriched"] == 1
|
|
assert result["metrics"]["created"] == 1
|
|
assert result["metrics"]["errors"] == 0
|
|
assert state.last_sync_timestamp is not None
|
|
|
|
|
|
@responses.activate
|
|
def test_dry_run_does_not_write_crm():
|
|
contacts = [
|
|
{
|
|
"id": 2,
|
|
"first_name": "Ada",
|
|
"last_name": "Lovelace",
|
|
"email": "ada@example.com",
|
|
"updated_at": "2026-07-04T12:00:00Z",
|
|
}
|
|
]
|
|
responses.add(
|
|
responses.GET,
|
|
f"{BASE_URL}/contacts.json",
|
|
json={"contacts": contacts, "total_count": 1},
|
|
status=200,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
state_file = Path(tmpdir) / "state.json"
|
|
redmine = RedmineClient(BASE_URL, "secret", project_id=1)
|
|
crm = CrmClient()
|
|
crm._store.clear()
|
|
state = sync_redmine_contacts.FileSyncState(state_file)
|
|
|
|
result = sync_redmine_contacts.run_pipeline(
|
|
redmine=redmine,
|
|
crm=crm,
|
|
state=state,
|
|
batch_size=100,
|
|
dry_run=True,
|
|
)
|
|
|
|
assert result["dry_run"] is True
|
|
assert result["metrics"]["created"] == 0
|
|
assert not crm._store
|
|
|
|
|
|
@responses.activate
|
|
def test_pipeline_uses_last_sync_timestamp():
|
|
contacts = [
|
|
{
|
|
"id": 3,
|
|
"first_name": "Alan",
|
|
"last_name": "Turing",
|
|
"email": "alan@example.com",
|
|
"updated_at": "2026-07-05T12:00:00Z",
|
|
}
|
|
]
|
|
responses.add(
|
|
responses.GET,
|
|
f"{BASE_URL}/contacts.json",
|
|
json={"contacts": contacts, "total_count": 1},
|
|
status=200,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
state_file = Path(tmpdir) / "state.json"
|
|
state_file.write_text(json.dumps({"last_sync_timestamp": "2026-07-04T00:00:00Z"}), encoding="utf-8")
|
|
redmine = RedmineClient(BASE_URL, "secret", project_id=1)
|
|
crm = CrmClient()
|
|
crm._store.clear()
|
|
state = sync_redmine_contacts.FileSyncState(state_file)
|
|
|
|
result = sync_redmine_contacts.run_pipeline(
|
|
redmine=redmine,
|
|
crm=crm,
|
|
state=state,
|
|
batch_size=100,
|
|
dry_run=False,
|
|
)
|
|
|
|
assert result["metrics"]["fetched"] == 1
|
|
request = responses.calls[0].request
|
|
assert "updated_on" in (request.url or "")
|
|
|
|
|
|
def test_cli_requires_api_key():
|
|
env = {"REDMINE_API_KEY": ""}
|
|
with mock.patch.dict(os.environ, env, clear=True):
|
|
with pytest.raises(RuntimeError, match="REDMINE_API_KEY"):
|
|
sync_redmine_contacts._redmine_client_from_args(
|
|
sync_redmine_contacts.build_argument_parser().parse_args(["--base-url", BASE_URL, "--dry-run"])
|
|
)
|
|
|
|
|
|
def test_file_sync_state_persists_and_reads():
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
state_file = Path(tmpdir) / "state.json"
|
|
state = sync_redmine_contacts.FileSyncState(state_file)
|
|
metrics = sync_redmine_contacts.SyncMetrics()
|
|
metrics.fetched = 5
|
|
state.record_run("2026-07-05T08:00:00Z", metrics)
|
|
|
|
new_state = sync_redmine_contacts.FileSyncState(state_file)
|
|
assert new_state.last_sync_timestamp == "2026-07-05T08:00:00Z"
|
|
assert new_state.last_metrics is not None
|
|
assert new_state.last_metrics["fetched"] == 5
|
|
|
|
|
|
def test_metrics_aggregate_counts():
|
|
metrics = sync_redmine_contacts.SyncMetrics()
|
|
metrics.fetched = 10
|
|
metrics.enriched = 9
|
|
metrics.skipped = 1
|
|
metrics.created = 5
|
|
metrics.updated = 3
|
|
metrics.unchanged = 1
|
|
metrics.errors = 0
|
|
metrics.finished_at = sync_redmine_contacts._utc_now_iso()
|
|
|
|
d = metrics.to_dict()
|
|
assert d["fetched"] == 10
|
|
assert d["enriched"] == 9
|
|
assert d["created"] + d["updated"] + d["unchanged"] == 9
|