import asyncio import io import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) def test_router_simple_create_contact_supports_extended_payload_and_company_links(monkeypatch): from app.contacts.backend import router_simple insert_calls = [] update_calls = [] def fake_execute_query(query, params=None): if "UPDATE contact_companies" in query: update_calls.append((query, params)) return [] def fake_execute_insert(query, params=None): insert_calls.append((query, params)) if "INSERT INTO contacts" in query: return 321 return 1 async def fake_get_contact(contact_id): return {"id": contact_id, "first_name": "Ada"} monkeypatch.setattr(router_simple, "execute_query", fake_execute_query) monkeypatch.setattr(router_simple, "execute_insert", fake_execute_insert) monkeypatch.setattr(router_simple, "get_contact", fake_get_contact) payload = router_simple.ContactCreate( first_name="Ada", last_name="Lovelace", email="ada@example.com", phone="11111111", mobile="22222222", title="CTO", department="IT", company_ids=[10, 20], is_primary=True, role="Decision maker", is_active=True, ) created = asyncio.run(router_simple.create_contact(payload)) assert created["id"] == 321 assert any("INSERT INTO contacts" in query for query, _ in insert_calls) company_link_params = [params for query, params in insert_calls if "INSERT INTO contact_companies" in query] assert len(company_link_params) == 2 assert company_link_params[0][1] == 10 assert company_link_params[1][1] == 20 assert update_calls def test_contact_email_regex_uses_exact_address_boundaries(): import re from app.contacts.backend.router_simple import _exact_email_pattern pattern = re.compile(_exact_email_pattern("ada@example.com")) assert pattern.search("Ada , other@example.com") assert not pattern.search("notada@example.com") assert not pattern.search("ada@example.com.evil.test") def test_contact_detail_has_cases_and_email_tabs(): template = Path("app/contacts/frontend/contact_detail.html").read_text(encoding="utf-8") assert 'href="#cases"' in template assert 'href="#emails"' in template assert "/cases?limit=${contactRelatedPageSize}" in template assert "/emails?limit=${contactRelatedPageSize}" in template def test_contact_email_analysis_returns_review_only_changes(): from app.contacts.backend.router_simple import _contact_suggestions_from_email contact = { "first_name": "Ada", "last_name": "Lovelace", "email": "ada@old.example", "phone": "11111111", "mobile": None, "title": "Udvikler", "department": None, } parsed = { "sender_name": "Ada Lovelace ", "sender_email": "ada@new.example", "recipient_email": "support@bmc.example", "body_text": "Hej\n\nMobil: +45 22 33 44 55\nTitel: CTO\nAfdeling: IT\n", } suggestions = _contact_suggestions_from_email(contact, parsed) by_field = {item["field"]: item for item in suggestions} assert by_field["email"]["suggested"] == "ada@new.example" assert by_field["mobile"]["suggested"] == "+45 22 33 44 55" assert by_field["title"]["suggested"] == "CTO" assert by_field["department"]["suggested"] == "IT" assert "first_name" not in by_field assert "last_name" not in by_field assert "phone" not in by_field def test_contact_email_analysis_does_not_treat_our_sender_as_the_contact(): from app.contacts.backend.router_simple import _contact_suggestions_from_email contact = {"email": "customer@example.com"} parsed = { "sender_name": "Support Agent", "sender_email": "support@bmc.example", "recipient_email": "Customer ", "body_text": "Venlig hilsen", } assert _contact_suggestions_from_email(contact, parsed) == [] def test_contact_email_analysis_handles_createx_outlook_signature(): from app.contacts.backend.router_simple import _contact_suggestions_from_email contact = { "first_name": "Ida", "last_name": "Gundersen", "email": "ida@createx-onstage.com", "mobile": None, "title": None, } parsed = { "sender_name": "Ida ", "sender_email": "ida@createx-onstage.com", "recipient_email": "support@example.com", "body_text": """Ida Kind regards **Ida Gundersen** *Technical Advisor & Co-owner* **Mobile:** +45 42 25 59 08 **DK: **+45 55 86 05 00 **FI:** +358 40 550 5865 **NO:** +47 62 41 84 05 **Email:** ida\\@createx-onstage.com """, } by_field = { item["field"]: item for item in _contact_suggestions_from_email(contact, parsed) } assert by_field["mobile"]["suggested"] == "+45 42 25 59 08" assert by_field["title"]["suggested"] == "Technical Advisor & Co-owner" def test_contact_detail_has_outlook_dropzone_and_review_modal(): template = Path("app/contacts/frontend/contact_detail.html").read_text(encoding="utf-8") assert 'id="contactEmailDropzone"' in template assert 'accept=".msg,.eml,message/rfc822,application/vnd.ms-outlook"' in template assert "/analyze-email" in template assert 'id="contactEmailSuggestionsModal"' in template assert "applyContactEmailSuggestions()" in template def test_new_contact_email_analysis_extracts_company_cvr_and_name(): from app.contacts.backend.router_simple import _company_from_email_body company = _company_from_email_body( "Ida Gundersen\nTechnical Advisor & Co-owner\nCreatex ApS\nStoregade 4C | 4780 Stege\nCVR: 12 34 56 78", "Ida Gundersen", ) assert company == {"name": "Createx ApS", "cvr_number": "12345678"} def test_contacts_page_can_create_contact_from_email(): template = Path("app/contacts/frontend/contacts.html").read_text(encoding="utf-8") assert "Træk Outlook-mail hertil" in template assert 'id="createFromEmailInput"' in template assert 'id="createFromEmailDropzone"' in template assert "event.dataTransfer?.files?.[0]" in template assert "initializeCreateFromEmailDropzone()" in template assert "'/api/v1/contacts/analyze-email'" in template assert "'/api/v1/contacts/resolve-email-company'" in template assert 'id="createFromEmailModal"' in template def test_new_contact_email_uses_existing_cvr_lookup(monkeypatch): from starlette.datastructures import UploadFile from app.contacts.backend import router_simple from app.services.email_service import EmailService parsed = { "sender_name": "Ida Gundersen", "sender_email": "ida@example.com", "recipient_email": "support@example.com", "subject": "Hej", "body_text": "Ida Gundersen\nRådgiver\nForkert navn\nStoregade 4, 4780 Stege\nCVR: 12345678", } class FakeCvrService: async def lookup_by_cvr(self, cvr): assert cvr == "12345678" return {"name": "Officielt Firma ApS", "address": "Torvet 1", "postal_code": "4780", "city": "Stege", "source": "firmaapi"} monkeypatch.setattr(EmailService, "parse_eml_file", lambda self, content: parsed) monkeypatch.setattr(router_simple, "get_cvr_service", lambda: FakeCvrService()) monkeypatch.setattr(router_simple, "execute_query_single", lambda *args, **kwargs: None) result = asyncio.run(router_simple.analyze_email_for_new_contact( UploadFile(filename="mail.eml", file=io.BytesIO(b"mail")) )) assert result["company"]["lookup_found"] is True assert result["company"]["name"] == "Officielt Firma ApS" assert result["company"]["address"] == "Torvet 1" def test_contacts_without_search_uses_valid_neutral_ordering(): source = Path("app/contacts/backend/router_simple.py").read_text(encoding="utf-8") assert 'rank_order_sql = ""' in source assert "ORDER BY {rank_order_sql} c.last_name" in source assert 'rank_sql = "0"' not in source