fix: ruff lint + format fixes in tests, ESLint fixes in frontend

This commit is contained in:
2026-07-17 21:28:58 +02:00
parent 341d0c6f38
commit fbb1b39b57
56 changed files with 1399 additions and 721 deletions
+169 -55
View File
@@ -1,15 +1,11 @@
"""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.models.contact import Contact
from app.utils.ust_validation import validate_vat_id
@@ -115,7 +111,9 @@ class TestContactList:
"""GET /api/v1/contacts tests."""
@pytest.mark.asyncio
async def test_list_contacts_returns_200_with_pagination(self, admin_client, created_contact):
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
@@ -136,7 +134,9 @@ class TestContactList:
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)
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")
@@ -152,9 +152,13 @@ class TestContactList:
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)
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)
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")
@@ -194,7 +198,9 @@ class TestContactList:
assert item["address_country"] == "DE"
@pytest.mark.asyncio
async def test_list_contacts_search_by_company_name(self, admin_client, created_contact):
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
@@ -220,7 +226,9 @@ class TestContactList:
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):
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)
@@ -251,7 +259,9 @@ 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):
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
@@ -271,9 +281,13 @@ class TestContactDetail:
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):
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']}")
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
@@ -285,7 +299,9 @@ class TestContactCreate:
@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)
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"]
@@ -294,21 +310,27 @@ class TestContactCreate:
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):
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):
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):
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")
@@ -317,7 +339,9 @@ class TestContactCreate:
assert response.json()["vat_id"] is None
@pytest.mark.asyncio
async def test_create_contact_with_contact_persons(self, admin_client, sample_contact_data):
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,
@@ -337,13 +361,19 @@ class TestContactCreate:
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):
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"})
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):
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)
@@ -376,7 +406,9 @@ class TestContactUpdate:
assert response.status_code == 404
@pytest.mark.asyncio
async def test_update_contact_invalid_vat_id_returns_422(self, admin_client, created_contact):
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']}",
@@ -385,7 +417,9 @@ class TestContactUpdate:
assert response.status_code == 422
@pytest.mark.asyncio
async def test_update_contact_no_fields_returns_400(self, admin_client, created_contact):
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']}",
@@ -400,7 +434,9 @@ class TestContactDelete:
@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']}")
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
@@ -415,7 +451,9 @@ class TestContactDelete:
@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']}")
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
@@ -445,7 +483,9 @@ class TestContactPersons:
assert data["id"] is not None
@pytest.mark.asyncio
async def test_add_contact_person_to_nonexistent_contact_returns_404(self, admin_client):
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(
@@ -455,7 +495,9 @@ class TestContactPersons:
assert response.status_code == 404
@pytest.mark.asyncio
async def test_remove_contact_person_returns_204(self, admin_client, created_contact):
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(
@@ -472,7 +514,9 @@ class TestContactPersons:
assert del_resp.status_code == 204
@pytest.mark.asyncio
async def test_remove_nonexistent_contact_person_returns_404(self, admin_client, created_contact):
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(
@@ -506,25 +550,37 @@ class TestContactRBAC:
assert response.status_code == 401
@pytest.mark.asyncio
async def test_create_contact_as_admin_returns_201(self, admin_client, sample_contact_data):
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)
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):
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)
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):
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):
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']}",
@@ -533,18 +589,26 @@ class TestContactRBAC:
assert response.status_code == 200
@pytest.mark.asyncio
async def test_delete_contact_as_verkaeufer_returns_200(self, verkaeufer_client, sample_contact_data):
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)
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):
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)
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(
@@ -554,9 +618,13 @@ class TestContactRBAC:
assert person_resp.status_code == 201
@pytest.mark.asyncio
async def test_remove_person_as_verkaeufer_returns_204(self, verkaeufer_client, sample_contact_data):
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)
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",
@@ -569,7 +637,9 @@ class TestContactRBAC:
assert del_resp.status_code == 204
@pytest.mark.asyncio
async def test_update_nonexistent_contact_returns_404_as_verkaeufer(self, verkaeufer_client):
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(
@@ -579,14 +649,18 @@ class TestContactRBAC:
assert response.status_code == 404
@pytest.mark.asyncio
async def test_delete_nonexistent_contact_returns_404_as_verkaeufer(self, verkaeufer_client):
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):
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(
@@ -596,9 +670,13 @@ class TestContactRBAC:
assert response.status_code == 404
@pytest.mark.asyncio
async def test_remove_nonexistent_person_returns_404_as_verkaeufer(self, verkaeufer_client, sample_contact_data):
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)
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(
@@ -614,6 +692,7 @@ class TestContactServiceDirect:
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",
@@ -662,22 +741,29 @@ class TestContactServiceDirect:
)
db_session.add(contact4)
await db_session.commit()
results, total = await contact_service.list_contacts(db_session, is_private=True)
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")
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")
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
@@ -685,6 +771,7 @@ class TestContactServiceDirect:
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",
@@ -702,13 +789,17 @@ class TestContactServiceDirect:
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"})
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
@@ -716,20 +807,27 @@ class TestContactServiceDirect:
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"})
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())
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",
@@ -739,7 +837,11 @@ class TestContactServiceDirect:
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"})
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"
@@ -747,6 +849,7 @@ class TestContactServiceDirect:
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",
@@ -763,6 +866,7 @@ class TestContactServiceDirect:
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",
@@ -772,17 +876,22 @@ class TestContactServiceDirect:
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"})
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)
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"
@@ -790,6 +899,7 @@ class TestContactServiceDirect:
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
@@ -797,6 +907,7 @@ class TestContactServiceDirect:
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
@@ -804,6 +915,7 @@ class TestContactServiceDirect:
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")
@@ -811,6 +923,7 @@ class TestContactServiceDirect:
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
@@ -821,6 +934,7 @@ class TestContactServiceDirect:
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