"""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