"""Tests for contact CRUD, search, filter, and contact person endpoints.""" import uuid from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient from app.database import Base, get_db from app.main import app from app.models.contact import Contact, ContactPerson from app.utils.ust_validation import validate_vat_id @pytest_asyncio.fixture async def sample_contact_data(): """Valid contact data for creation.""" return { "company_name": "Müller Transport GmbH", "legal_form": "GmbH", "address_street": "Hauptstraße 1", "address_zip": "10115", "address_city": "Berlin", "address_country": "DE", "vat_id": "DE123456789", "phone": "+49 30 12345678", "email": "info@mueller-transport.de", "website": "https://mueller-transport.de", "role": "kaeufer", "is_private": False, } @pytest_asyncio.fixture async def sample_eu_contact_data(): """Valid EU contact data (non-DE).""" return { "company_name": "Van der Berg Logistics B.V.", "address_street": "Keizersgracht 100", "address_zip": "1015", "address_city": "Amsterdam", "address_country": "NL", "vat_id": "NL123456789B01", "email": "info@vandberg.nl", "role": "verkaeufer", "is_private": False, } @pytest_asyncio.fixture async def sample_beide_contact_data(): """Valid contact with role 'beide'.""" return { "company_name": "Schmidt & Söhne KG", "address_city": "Hamburg", "address_country": "DE", "role": "beide", "is_private": False, } @pytest_asyncio.fixture async def created_contact(admin_client, sample_contact_data): """Create a contact via API and return the response.""" response = await admin_client.post("/api/v1/contacts/", json=sample_contact_data) assert response.status_code == 201, response.text return response.json() class TestUstValidation: """Unit tests for USt-IdNr. validation utility.""" def test_validate_de_vat_id_valid(self): assert validate_vat_id("DE123456789") is True def test_validate_de_vat_id_invalid_short(self): assert validate_vat_id("DE12345678") is False def test_validate_de_vat_id_invalid_long(self): assert validate_vat_id("DE1234567890") is False def test_validate_at_vat_id_valid(self): assert validate_vat_id("ATU12345678") is True def test_validate_nl_vat_id_valid(self): assert validate_vat_id("NL123456789B01") is True def test_validate_fr_vat_id_valid(self): assert validate_vat_id("FRAB123456789") is True def test_validate_it_vat_id_valid(self): assert validate_vat_id("IT12345678901") is True def test_validate_es_vat_id_valid(self): assert validate_vat_id("ESA1234567B") is True def test_validate_empty_vat_id(self): assert validate_vat_id("") is True def test_validate_none_vat_id(self): assert validate_vat_id(None) is True def test_validate_non_eu_country(self): assert validate_vat_id("US123456789") is False def test_validate_lowercase_normalised(self): assert validate_vat_id("de123456789") is True def test_validate_with_spaces(self): assert validate_vat_id("DE 123 456 789") is True class TestContactList: """GET /api/v1/contacts tests.""" @pytest.mark.asyncio async def test_list_contacts_returns_200_with_pagination(self, admin_client, created_contact): """GET /api/v1/contacts returns 200 with paginated list.""" response = await admin_client.get("/api/v1/contacts/?page=1&page_size=20") assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data assert "page" in data assert "page_size" in data assert data["total"] >= 1 assert len(data["items"]) >= 1 @pytest.mark.asyncio async def test_list_contacts_filter_by_role_kaeufer_includes_beide( self, admin_client, sample_contact_data, sample_beide_contact_data ): """GET /api/v1/contacts?role=kaeufer returns kaeufer + beide contacts.""" # Create a kaeufer contact resp1 = await admin_client.post("/api/v1/contacts/", json=sample_contact_data) assert resp1.status_code == 201 # Create a beide contact resp2 = await admin_client.post("/api/v1/contacts/", json=sample_beide_contact_data) assert resp2.status_code == 201 response = await admin_client.get("/api/v1/contacts/?role=kaeufer") assert response.status_code == 200 data = response.json() roles = [c["role"] for c in data["items"]] assert "kaeufer" in roles assert "beide" in roles assert "verkaeufer" not in roles @pytest.mark.asyncio async def test_list_contacts_filter_by_role_verkaeufer_includes_beide( self, admin_client, sample_eu_contact_data, sample_beide_contact_data ): """GET /api/v1/contacts?role=verkaeufer returns verkaeufer + beide contacts.""" resp1 = await admin_client.post("/api/v1/contacts/", json=sample_eu_contact_data) assert resp1.status_code == 201 resp2 = await admin_client.post("/api/v1/contacts/", json=sample_beide_contact_data) assert resp2.status_code == 201 response = await admin_client.get("/api/v1/contacts/?role=verkaeufer") assert response.status_code == 200 data = response.json() roles = [c["role"] for c in data["items"]] assert "verkaeufer" in roles assert "beide" in roles assert "kaeufer" not in roles @pytest.mark.asyncio async def test_list_contacts_filter_is_eu_true( self, admin_client, sample_contact_data, sample_eu_contact_data ): """GET /api/v1/contacts?is_eu=true returns only non-DE contacts.""" await admin_client.post("/api/v1/contacts/", json=sample_contact_data) await admin_client.post("/api/v1/contacts/", json=sample_eu_contact_data) response = await admin_client.get("/api/v1/contacts/?is_eu=true") assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["address_country"] != "DE" @pytest.mark.asyncio async def test_list_contacts_filter_is_eu_false( self, admin_client, sample_contact_data, sample_eu_contact_data ): """GET /api/v1/contacts?is_eu=false returns only DE contacts.""" await admin_client.post("/api/v1/contacts/", json=sample_contact_data) await admin_client.post("/api/v1/contacts/", json=sample_eu_contact_data) response = await admin_client.get("/api/v1/contacts/?is_eu=false") assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["address_country"] == "DE" @pytest.mark.asyncio async def test_list_contacts_search_by_company_name(self, admin_client, created_contact): """GET /api/v1/contacts?search=mueller returns matching contacts.""" response = await admin_client.get("/api/v1/contacts/?search=mueller") assert response.status_code == 200 data = response.json() assert data["total"] >= 1 assert any("Müller" in c["company_name"] for c in data["items"]) @pytest.mark.asyncio async def test_list_contacts_search_by_city(self, admin_client, created_contact): """GET /api/v1/contacts?search=berlin returns matching contacts.""" response = await admin_client.get("/api/v1/contacts/?search=berlin") assert response.status_code == 200 data = response.json() assert data["total"] >= 1 @pytest.mark.asyncio async def test_list_contacts_search_no_results(self, admin_client): """GET /api/v1/contacts?search=nonexistent returns empty list.""" response = await admin_client.get("/api/v1/contacts/?search=nonexistent_xyz") assert response.status_code == 200 data = response.json() assert data["total"] == 0 assert len(data["items"]) == 0 @pytest.mark.asyncio async def test_list_contacts_sort_by_company_name(self, admin_client, sample_contact_data, sample_eu_contact_data): """GET /api/v1/contacts?sort=company_name returns sorted list.""" await admin_client.post("/api/v1/contacts/", json=sample_contact_data) await admin_client.post("/api/v1/contacts/", json=sample_eu_contact_data) response = await admin_client.get("/api/v1/contacts/?sort=company_name") assert response.status_code == 200 data = response.json() names = [c["company_name"] for c in data["items"]] assert names == sorted(names) @pytest.mark.asyncio async def test_list_contacts_pagination(self, admin_client, sample_contact_data): """GET /api/v1/contacts?page=1&page_size=1 returns correct pagination.""" for i in range(3): data = {**sample_contact_data, "company_name": f"Company {i} GmbH"} await admin_client.post("/api/v1/contacts/", json=data) response = await admin_client.get("/api/v1/contacts/?page=1&page_size=1") assert response.status_code == 200 data = response.json() assert data["page"] == 1 assert data["page_size"] == 1 assert len(data["items"]) == 1 assert data["total"] >= 3 class TestContactDetail: """GET /api/v1/contacts/:id tests.""" @pytest.mark.asyncio async def test_get_contact_returns_200_with_detail(self, admin_client, created_contact): """GET /api/v1/contacts/:id returns 200 with contact detail.""" response = await admin_client.get(f"/api/v1/contacts/{created_contact['id']}") assert response.status_code == 200 data = response.json() assert data["id"] == created_contact["id"] assert data["company_name"] == "Müller Transport GmbH" assert "contact_persons" in data assert isinstance(data["contact_persons"], list) @pytest.mark.asyncio async def test_get_contact_nonexistent_returns_404(self, admin_client): """GET /api/v1/contacts/:nonexistent returns 404.""" fake_id = uuid.uuid4() response = await admin_client.get(f"/api/v1/contacts/{fake_id}") assert response.status_code == 404 data = response.json() assert data["detail"]["error"]["code"] == "CONTACT_NOT_FOUND" @pytest.mark.asyncio async def test_get_contact_after_soft_delete_returns_404(self, admin_client, created_contact): """GET /api/v1/contacts/:id after soft-delete returns 404.""" del_resp = await admin_client.delete(f"/api/v1/contacts/{created_contact['id']}") assert del_resp.status_code == 200 get_resp = await admin_client.get(f"/api/v1/contacts/{created_contact['id']}") assert get_resp.status_code == 404 class TestContactCreate: """POST /api/v1/contacts tests.""" @pytest.mark.asyncio async def test_create_contact_returns_201(self, admin_client, sample_contact_data): """POST /api/v1/contacts with valid data returns 201.""" response = await admin_client.post("/api/v1/contacts/", json=sample_contact_data) assert response.status_code == 201 data = response.json() assert data["company_name"] == sample_contact_data["company_name"] assert data["role"] == "kaeufer" assert data["vat_id_status"] == "ungeprueft" assert data["id"] is not None @pytest.mark.asyncio async def test_create_contact_with_invalid_vat_id_returns_422(self, admin_client, sample_contact_data): """POST /api/v1/contacts with invalid VAT ID format returns 422.""" data = {**sample_contact_data, "vat_id": "INVALID123"} response = await admin_client.post("/api/v1/contacts/", json=data) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_contact_with_de_vat_too_short_returns_422(self, admin_client, sample_contact_data): """POST /api/v1/contacts with too-short DE VAT ID returns 422.""" data = {**sample_contact_data, "vat_id": "DE12345678"} response = await admin_client.post("/api/v1/contacts/", json=data) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_contact_with_no_vat_id_returns_201(self, admin_client, sample_contact_data): """POST /api/v1/contacts without VAT ID returns 201.""" data = {**sample_contact_data} data.pop("vat_id") response = await admin_client.post("/api/v1/contacts/", json=data) assert response.status_code == 201 assert response.json()["vat_id"] is None @pytest.mark.asyncio async def test_create_contact_with_contact_persons(self, admin_client, sample_contact_data): """POST /api/v1/contacts with nested contact persons returns 201.""" data = { **sample_contact_data, "contact_persons": [ { "name": "Hans Müller", "function": "Geschäftsführer", "phone": "+49 30 87654321", "email": "hans@mueller-transport.de", } ], } response = await admin_client.post("/api/v1/contacts/", json=data) assert response.status_code == 201 contact_data = response.json() assert len(contact_data["contact_persons"]) == 1 assert contact_data["contact_persons"][0]["name"] == "Hans Müller" @pytest.mark.asyncio async def test_create_contact_missing_required_fields_returns_422(self, admin_client): """POST /api/v1/contacts with missing required fields returns 422.""" response = await admin_client.post("/api/v1/contacts/", json={"address_country": "DE"}) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_contact_invalid_role_returns_422(self, admin_client, sample_contact_data): """POST /api/v1/contacts with invalid role returns 422.""" data = {**sample_contact_data, "role": "invalid_role"} response = await admin_client.post("/api/v1/contacts/", json=data) assert response.status_code == 422 class TestContactUpdate: """PUT /api/v1/contacts/:id tests.""" @pytest.mark.asyncio async def test_update_contact_returns_200(self, admin_client, created_contact): """PUT /api/v1/contacts/:id with valid data returns 200.""" response = await admin_client.put( f"/api/v1/contacts/{created_contact['id']}", json={"company_name": "Müller Transport AG", "phone": "+49 30 99999999"}, ) assert response.status_code == 200 data = response.json() assert data["company_name"] == "Müller Transport AG" assert data["phone"] == "+49 30 99999999" @pytest.mark.asyncio async def test_update_contact_nonexistent_returns_404(self, admin_client): """PUT /api/v1/contacts/:nonexistent returns 404.""" fake_id = uuid.uuid4() response = await admin_client.put( f"/api/v1/contacts/{fake_id}", json={"company_name": "Test GmbH"}, ) assert response.status_code == 404 @pytest.mark.asyncio async def test_update_contact_invalid_vat_id_returns_422(self, admin_client, created_contact): """PUT /api/v1/contacts/:id with invalid VAT ID returns 422.""" response = await admin_client.put( f"/api/v1/contacts/{created_contact['id']}", json={"vat_id": "INVALID123"}, ) assert response.status_code == 422 @pytest.mark.asyncio async def test_update_contact_no_fields_returns_400(self, admin_client, created_contact): """PUT /api/v1/contacts/:id with no fields returns 400.""" response = await admin_client.put( f"/api/v1/contacts/{created_contact['id']}", json={}, ) assert response.status_code == 400 class TestContactDelete: """DELETE /api/v1/contacts/:id tests.""" @pytest.mark.asyncio async def test_delete_contact_returns_200(self, admin_client, created_contact): """DELETE /api/v1/contacts/:id returns 200 (soft delete).""" response = await admin_client.delete(f"/api/v1/contacts/{created_contact['id']}") assert response.status_code == 200 data = response.json() assert data["deleted_at"] is not None @pytest.mark.asyncio async def test_delete_contact_nonexistent_returns_404(self, admin_client): """DELETE /api/v1/contacts/:nonexistent returns 404.""" fake_id = uuid.uuid4() response = await admin_client.delete(f"/api/v1/contacts/{fake_id}") assert response.status_code == 404 @pytest.mark.asyncio async def test_deleted_contact_not_in_list(self, admin_client, created_contact): """Soft-deleted contact does not appear in list.""" del_resp = await admin_client.delete(f"/api/v1/contacts/{created_contact['id']}") assert del_resp.status_code == 200 list_resp = await admin_client.get("/api/v1/contacts/") assert list_resp.status_code == 200 ids = [c["id"] for c in list_resp.json()["items"]] assert created_contact["id"] not in ids class TestContactPersons: """Contact person management endpoints.""" @pytest.mark.asyncio async def test_add_contact_person_returns_201(self, admin_client, created_contact): """POST /api/v1/contacts/:id/persons returns 201.""" response = await admin_client.post( f"/api/v1/contacts/{created_contact['id']}/persons", json={ "name": "Anna Schmidt", "function": "Einkauf", "phone": "+49 30 11122233", "email": "anna@mueller-transport.de", }, ) assert response.status_code == 201 data = response.json() assert data["name"] == "Anna Schmidt" assert data["function"] == "Einkauf" assert data["id"] is not None @pytest.mark.asyncio async def test_add_contact_person_to_nonexistent_contact_returns_404(self, admin_client): """POST /api/v1/contacts/:nonexistent/persons returns 404.""" fake_id = uuid.uuid4() response = await admin_client.post( f"/api/v1/contacts/{fake_id}/persons", json={"name": "Test Person"}, ) assert response.status_code == 404 @pytest.mark.asyncio async def test_remove_contact_person_returns_204(self, admin_client, created_contact): """DELETE /api/v1/contacts/:id/persons/:person_id returns 204.""" # First add a person add_resp = await admin_client.post( f"/api/v1/contacts/{created_contact['id']}/persons", json={"name": "Test Person"}, ) assert add_resp.status_code == 201 person_id = add_resp.json()["id"] # Then remove it del_resp = await admin_client.delete( f"/api/v1/contacts/{created_contact['id']}/persons/{person_id}" ) assert del_resp.status_code == 204 @pytest.mark.asyncio async def test_remove_nonexistent_contact_person_returns_404(self, admin_client, created_contact): """DELETE /api/v1/contacts/:id/persons/:nonexistent returns 404.""" fake_person_id = uuid.uuid4() response = await admin_client.delete( f"/api/v1/contacts/{created_contact['id']}/persons/{fake_person_id}" ) assert response.status_code == 404 @pytest.mark.asyncio async def test_contact_detail_includes_persons(self, admin_client, created_contact): """GET /api/v1/contacts/:id includes contact persons in response.""" # Add a person await admin_client.post( f"/api/v1/contacts/{created_contact['id']}/persons", json={"name": "Max Mustermann", "function": "Vertrieb"}, ) # Get contact detail response = await admin_client.get(f"/api/v1/contacts/{created_contact['id']}") assert response.status_code == 200 data = response.json() assert len(data["contact_persons"]) >= 1 assert data["contact_persons"][0]["name"] == "Max Mustermann" class TestContactRBAC: """RBAC enforcement tests.""" @pytest.mark.asyncio async def test_list_contacts_requires_auth(self, client): """GET /api/v1/contacts without auth returns 401.""" response = await client.get("/api/v1/contacts/") assert response.status_code == 401 @pytest.mark.asyncio async def test_create_contact_as_admin_returns_201(self, admin_client, sample_contact_data): """POST /api/v1/contacts as admin returns 201.""" response = await admin_client.post("/api/v1/contacts/", json=sample_contact_data) assert response.status_code == 201 @pytest.mark.asyncio async def test_create_contact_as_verkaeufer_returns_201(self, verkaeufer_client, sample_contact_data): """POST /api/v1/contacts as verkaeufer returns 201.""" response = await verkaeufer_client.post("/api/v1/contacts/", json=sample_contact_data) assert response.status_code == 201 @pytest.mark.asyncio async def test_list_contacts_as_verkaeufer_returns_200(self, verkaeufer_client, created_contact): """GET /api/v1/contacts as verkaeufer returns 200 (read allowed).""" response = await verkaeufer_client.get("/api/v1/contacts/") assert response.status_code == 200 @pytest.mark.asyncio async def test_update_contact_as_verkaeufer_returns_200(self, verkaeufer_client, created_contact): """PUT /api/v1/contacts/:id as verkaeufer returns 200.""" response = await verkaeufer_client.put( f"/api/v1/contacts/{created_contact['id']}", json={"company_name": "Updated by Verkaeufer"}, ) assert response.status_code == 200 @pytest.mark.asyncio async def test_delete_contact_as_verkaeufer_returns_200(self, verkaeufer_client, sample_contact_data): """DELETE /api/v1/contacts/:id as verkaeufer returns 200.""" resp = await verkaeufer_client.post("/api/v1/contacts/", json=sample_contact_data) assert resp.status_code == 201 contact_id = resp.json()["id"] del_resp = await verkaeufer_client.delete(f"/api/v1/contacts/{contact_id}") assert del_resp.status_code == 200 @pytest.mark.asyncio async def test_add_person_as_verkaeufer_returns_201(self, verkaeufer_client, sample_contact_data): """POST /api/v1/contacts/:id/persons as verkaeufer returns 201.""" resp = await verkaeufer_client.post("/api/v1/contacts/", json=sample_contact_data) assert resp.status_code == 201 contact_id = resp.json()["id"] person_resp = await verkaeufer_client.post( f"/api/v1/contacts/{contact_id}/persons", json={"name": "Test Person"}, ) assert person_resp.status_code == 201 @pytest.mark.asyncio async def test_remove_person_as_verkaeufer_returns_204(self, verkaeufer_client, sample_contact_data): """DELETE /api/v1/contacts/:id/persons/:pid as verkaeufer returns 204.""" resp = await verkaeufer_client.post("/api/v1/contacts/", json=sample_contact_data) contact_id = resp.json()["id"] person_resp = await verkaeufer_client.post( f"/api/v1/contacts/{contact_id}/persons", json={"name": "To Remove"}, ) person_id = person_resp.json()["id"] del_resp = await verkaeufer_client.delete( f"/api/v1/contacts/{contact_id}/persons/{person_id}" ) assert del_resp.status_code == 204 @pytest.mark.asyncio async def test_update_nonexistent_contact_returns_404_as_verkaeufer(self, verkaeufer_client): """PUT /api/v1/contacts/:nonexistent as verkaeufer returns 404.""" fake_id = uuid.uuid4() response = await verkaeufer_client.put( f"/api/v1/contacts/{fake_id}", json={"company_name": "Test"}, ) assert response.status_code == 404 @pytest.mark.asyncio async def test_delete_nonexistent_contact_returns_404_as_verkaeufer(self, verkaeufer_client): """DELETE /api/v1/contacts/:nonexistent as verkaeufer returns 404.""" fake_id = uuid.uuid4() response = await verkaeufer_client.delete(f"/api/v1/contacts/{fake_id}") assert response.status_code == 404 @pytest.mark.asyncio async def test_add_person_to_nonexistent_contact_returns_404_as_verkaeufer(self, verkaeufer_client): """POST /api/v1/contacts/:nonexistent/persons as verkaeufer returns 404.""" fake_id = uuid.uuid4() response = await verkaeufer_client.post( f"/api/v1/contacts/{fake_id}/persons", json={"name": "Test"}, ) assert response.status_code == 404 @pytest.mark.asyncio async def test_remove_nonexistent_person_returns_404_as_verkaeufer(self, verkaeufer_client, sample_contact_data): """DELETE /api/v1/contacts/:id/persons/:nonexistent as verkaeufer returns 404.""" resp = await verkaeufer_client.post("/api/v1/contacts/", json=sample_contact_data) contact_id = resp.json()["id"] fake_person_id = uuid.uuid4() response = await verkaeufer_client.delete( f"/api/v1/contacts/{contact_id}/persons/{fake_person_id}" ) assert response.status_code == 404 class TestContactServiceDirect: """Direct service-level tests for coverage.""" @pytest.mark.asyncio async def test_service_list_contacts_with_all_filters(self, db_session): """Test list_contacts with all filter parameters.""" from app.services import contact_service contact1 = Contact( company_name="Alpha GmbH", address_city="Berlin", address_country="DE", role="kaeufer", ) contact2 = Contact( company_name="Beta B.V.", address_city="Amsterdam", address_country="NL", role="verkaeufer", ) contact3 = Contact( company_name="Gamma KG", address_city="Hamburg", address_country="DE", role="beide", ) db_session.add_all([contact1, contact2, contact3]) await db_session.commit() # Test search results, total = await contact_service.list_contacts(db_session, search="alpha") assert total == 1 assert results[0].company_name == "Alpha GmbH" # Test role filter (kaeufer includes beide) results, total = await contact_service.list_contacts(db_session, role="kaeufer") assert total == 2 # Test is_eu filter results, total = await contact_service.list_contacts(db_session, is_eu=True) assert total == 1 assert results[0].address_country == "NL" # Test is_eu=false (Inland) results, total = await contact_service.list_contacts(db_session, is_eu=False) assert total == 2 # Test is_private filter contact4 = Contact( company_name="Private Person", address_country="DE", role="kaeufer", is_private=True, ) db_session.add(contact4) await db_session.commit() results, total = await contact_service.list_contacts(db_session, is_private=True) assert total == 1 # Test sort descending results, total = await contact_service.list_contacts(db_session, sort="-company_name") names = [r.company_name for r in results] assert names == sorted(names, reverse=True) # Test invalid sort field falls back to created_at results, total = await contact_service.list_contacts(db_session, sort="invalid_field") assert total >= 4 @pytest.mark.asyncio async def test_service_get_contact_by_id_not_found(self, db_session): """Test get_contact_by_id returns None for nonexistent ID.""" from app.services import contact_service result = await contact_service.get_contact_by_id(db_session, uuid.uuid4()) assert result is None @pytest.mark.asyncio async def test_service_create_contact_with_persons(self, db_session): """Test create_contact with nested contact persons.""" from app.services import contact_service data = { "company_name": "Test Service GmbH", "address_country": "DE", "role": "kaeufer", "contact_persons": [ {"name": "Person 1", "function": "CEO"}, {"name": "Person 2", "phone": "+49 30 123"}, ], } contact = await contact_service.create_contact(db_session, data) assert contact.company_name == "Test Service GmbH" assert len(contact.contact_persons) == 2 @pytest.mark.asyncio async def test_service_update_contact_not_found(self, db_session): """Test update_contact returns None for nonexistent ID.""" from app.services import contact_service result = await contact_service.update_contact(db_session, uuid.uuid4(), {"company_name": "Test"}) assert result is None @pytest.mark.asyncio async def test_service_soft_delete_contact_not_found(self, db_session): """Test soft_delete_contact returns None for nonexistent ID.""" from app.services import contact_service result = await contact_service.soft_delete_contact(db_session, uuid.uuid4()) assert result is None @pytest.mark.asyncio async def test_service_add_contact_person_not_found(self, db_session): """Test add_contact_person returns None for nonexistent contact.""" from app.services import contact_service result = await contact_service.add_contact_person(db_session, uuid.uuid4(), {"name": "Test"}) assert result is None @pytest.mark.asyncio async def test_service_remove_contact_person_not_found(self, db_session): """Test remove_contact_person returns False for nonexistent person.""" from app.services import contact_service result = await contact_service.remove_contact_person(db_session, uuid.uuid4(), uuid.uuid4()) assert result is False @pytest.mark.asyncio async def test_service_update_contact_success(self, db_session): """Test update_contact successfully updates fields.""" from app.services import contact_service contact = Contact( company_name="Original GmbH", address_country="DE", role="kaeufer", ) db_session.add(contact) await db_session.commit() await db_session.refresh(contact) updated = await contact_service.update_contact(db_session, contact.id, {"company_name": "Updated GmbH", "phone": "+49 30 999"}) assert updated.company_name == "Updated GmbH" assert updated.phone == "+49 30 999" @pytest.mark.asyncio async def test_service_soft_delete_contact_success(self, db_session): """Test soft_delete_contact sets deleted_at.""" from app.services import contact_service contact = Contact( company_name="To Delete GmbH", address_country="DE", role="kaeufer", ) db_session.add(contact) await db_session.commit() await db_session.refresh(contact) deleted = await contact_service.soft_delete_contact(db_session, contact.id) assert deleted.deleted_at is not None @pytest.mark.asyncio async def test_service_add_and_remove_contact_person(self, db_session): """Test add_contact_person and remove_contact_person.""" from app.services import contact_service contact = Contact( company_name="Person Test GmbH", address_country="DE", role="kaeufer", ) db_session.add(contact) await db_session.commit() await db_session.refresh(contact) person = await contact_service.add_contact_person(db_session, contact.id, {"name": "Test Person", "function": "Manager"}) assert person is not None assert person.name == "Test Person" removed = await contact_service.remove_contact_person(db_session, contact.id, person.id) assert removed is True @pytest.mark.asyncio async def test_ust_validation_or_raise_valid(self): """Test validate_vat_id_or_raise with valid VAT ID.""" from app.utils.ust_validation import validate_vat_id_or_raise result = validate_vat_id_or_raise("DE123456789") assert result == "DE123456789" @pytest.mark.asyncio async def test_ust_validation_or_raise_none(self): """Test validate_vat_id_or_raise with None.""" from app.utils.ust_validation import validate_vat_id_or_raise result = validate_vat_id_or_raise(None) assert result is None @pytest.mark.asyncio async def test_ust_validation_or_raise_empty(self): """Test validate_vat_id_or_raise with empty string.""" from app.utils.ust_validation import validate_vat_id_or_raise result = validate_vat_id_or_raise("") assert result is None @pytest.mark.asyncio async def test_ust_validation_or_raise_invalid(self): """Test validate_vat_id_or_raise raises ValueError for invalid format.""" from app.utils.ust_validation import validate_vat_id_or_raise with pytest.raises(ValueError, match="Invalid VAT ID format"): validate_vat_id_or_raise("DE123") @pytest.mark.asyncio async def test_ust_validation_get_country_code(self): """Test get_country_code_from_vat_id.""" from app.utils.ust_validation import get_country_code_from_vat_id assert get_country_code_from_vat_id("DE123456789") == "DE" assert get_country_code_from_vat_id("at123") == "AT" assert get_country_code_from_vat_id("") is None assert get_country_code_from_vat_id("A") is None assert get_country_code_from_vat_id("12") is None @pytest.mark.asyncio async def test_ust_validation_eu_fallback(self): """Test EU fallback pattern for countries without specific regex.""" from app.utils.ust_validation import validate_vat_id # Ireland (IE) is in EU set but has no specific pattern assert validate_vat_id("IE1234567AB") is True # Bulgaria (BG) is in EU set but has no specific pattern assert validate_vat_id("BG1234567890") is True # Too short for fallback assert validate_vat_id("BG123") is False