Files
leocrm/tests/test_dedup.py
T

217 lines
8.0 KiB
Python
Raw Normal View History

"""Deduplication / merge tests — Task 5.23."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
async def _get_csrf_token(client: AsyncClient) -> str:
"""Extract CSRF token from login response."""
# The login already happened, but we need the token from the response.
# Re-login to get a fresh token (or extract from the last response).
resp = await client.post(
"/api/v1/auth/login",
json={"email": "admin@tenanta.com", "password": "TestPass123!"},
headers=ORIGIN_HEADER,
)
return resp.json().get("csrf_token", "")
def _csrf_headers(token: str) -> dict:
"""Return headers with CSRF token."""
headers = dict(ORIGIN_HEADER)
headers["X-CSRF-Token"] = token
return headers
@pytest.mark.asyncio
class TestFindDuplicates:
"""POST /api/v1/contacts/duplicates — find duplicate contacts."""
async def test_find_duplicates_by_email(self, client: AsyncClient, db_session):
"""Two contacts with same email should be detected as duplicates."""
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
csrf = await _get_csrf_token(client)
hdrs = _csrf_headers(csrf)
# Create two contacts with same email
resp1 = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "John", "surname": "Doe", "email_1": "john@example.com"},
headers=hdrs,
)
assert resp1.status_code == 201, f"Create 1 failed: {resp1.text}"
resp2 = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "Johnny", "surname": "Doe", "email_1": "john@example.com"},
headers=hdrs,
)
assert resp2.status_code == 201, f"Create 2 failed: {resp2.text}"
# Find duplicates
resp = await client.post(
"/api/v1/contacts/duplicates",
json={"threshold": 0.5, "limit": 50},
headers=hdrs,
)
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) >= 1
pair = data[0]
assert "source_contact" in pair
assert "target_contact" in pair
assert "similarity_score" in pair
assert "match_reasons" in pair
assert "email_match" in pair["match_reasons"]
async def test_find_duplicates_empty(self, client: AsyncClient, db_session):
"""No duplicates when contacts are unique."""
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
csrf = await _get_csrf_token(client)
hdrs = _csrf_headers(csrf)
resp = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "Unique", "surname": "Person", "email_1": "unique@example.com"},
headers=hdrs,
)
assert resp.status_code == 201, f"Create failed: {resp.text}"
resp = await client.post(
"/api/v1/contacts/duplicates",
json={"threshold": 0.7, "limit": 50},
headers=hdrs,
)
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert len(data) == 0
@pytest.mark.asyncio
class TestMergeContacts:
"""POST /api/v1/contacts/merge — merge two contacts."""
async def test_merge_contacts_success(self, client: AsyncClient, db_session):
"""Merge source into target, source should be soft-deleted."""
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
csrf = await _get_csrf_token(client)
hdrs = _csrf_headers(csrf)
# Create source contact with phone
resp1 = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "Source", "surname": "Test", "email_1": "source@example.com", "phone_1": "+49123456789"},
headers=hdrs,
)
assert resp1.status_code == 201, f"Create source failed: {resp1.text}"
source_id = resp1.json()["id"]
# Create target contact without phone
resp2 = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "Target", "surname": "Test", "email_1": "target@example.com"},
headers=hdrs,
)
assert resp2.status_code == 201, f"Create target failed: {resp2.text}"
target_id = resp2.json()["id"]
# Merge
resp = await client.post(
"/api/v1/contacts/merge",
json={"source_contact_id": source_id, "target_contact_id": target_id},
headers=hdrs,
)
assert resp.status_code == 200, f"Merge failed: {resp.text}"
data = resp.json()
assert data["source_contact_id"] == source_id
assert data["target_contact_id"] == target_id
assert "merge_id" in data
assert "merged_fields" in data
# Phone should have been auto-merged
assert "phone_1" in data["merged_fields"]
# Source should be soft-deleted (not in list)
list_resp = await client.get("/api/v1/contacts", headers=ORIGIN_HEADER)
contact_ids = [c["id"] for c in list_resp.json()["items"]]
assert source_id not in contact_ids
assert target_id in contact_ids
async def test_merge_same_contact_fails(self, client: AsyncClient, db_session):
"""Merging a contact with itself should fail."""
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
csrf = await _get_csrf_token(client)
hdrs = _csrf_headers(csrf)
resp = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "Same", "surname": "Contact", "email_1": "same@example.com"},
headers=hdrs,
)
assert resp.status_code == 201, f"Create failed: {resp.text}"
contact_id = resp.json()["id"]
resp = await client.post(
"/api/v1/contacts/merge",
json={"source_contact_id": contact_id, "target_contact_id": contact_id},
headers=hdrs,
)
assert resp.status_code == 400
@pytest.mark.asyncio
class TestMergeHistory:
"""GET /api/v1/contacts/merge-history — merge history."""
async def test_merge_history_returns_records(self, client: AsyncClient, db_session):
"""After a merge, history should contain the record."""
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
csrf = await _get_csrf_token(client)
hdrs = _csrf_headers(csrf)
# Create and merge two contacts
resp1 = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "Hist", "surname": "Source", "email_1": "hist-source@example.com"},
headers=hdrs,
)
assert resp1.status_code == 201
source_id = resp1.json()["id"]
resp2 = await client.post(
"/api/v1/contacts",
json={"type": "person", "firstname": "Hist", "surname": "Target", "email_1": "hist-target@example.com"},
headers=hdrs,
)
assert resp2.status_code == 201
target_id = resp2.json()["id"]
merge_resp = await client.post(
"/api/v1/contacts/merge",
json={"source_contact_id": source_id, "target_contact_id": target_id, "note": "Test merge"},
headers=hdrs,
)
assert merge_resp.status_code == 200
# Get history (GET doesn't need CSRF)
resp = await client.get("/api/v1/contacts/merge-history", headers=ORIGIN_HEADER)
assert resp.status_code == 200
data = resp.json()
assert "items" in data
assert "total" in data
assert data["total"] >= 1
item = data["items"][0]
assert item["source_contact_id"] == source_id
assert item["target_contact_id"] == target_id
assert item["note"] == "Test merge"