From 96975f8dd974d7ea001e47c9c745e5b83865173d Mon Sep 17 00:00:00 2001 From: Mazen Tamer Salah <78306991+mazen-salah@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:50:22 +0300 Subject: [PATCH] fix(contacts): tolerate non-string body in /api/contacts/import (#3638) import_vcf built `text = data.get("vcf") or data.get("text") or ""`, so a non-string JSON value (a number, list, etc.) stayed in place and the following `text.strip()` raised AttributeError, returning HTTP 500. Coerce vcf/text/csv with str() so non-string input degrades to the existing structured "no data" response, matching the file's convention elsewhere. Adds tests/test_contacts_import_nonstring.py covering non-string vcf, non-string csv, and an empty body. --- routes/contacts_routes.py | 7 +++-- tests/test_contacts_import_nonstring.py | 39 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/test_contacts_import_nonstring.py diff --git a/routes/contacts_routes.py b/routes/contacts_routes.py index e4e8ce759..58a57a1e1 100644 --- a/routes/contacts_routes.py +++ b/routes/contacts_routes.py @@ -729,8 +729,11 @@ def setup_contacts_routes(): @router.post("/import") async def import_vcf(data: dict, _admin: str = Depends(require_admin)): """Import contacts from .vcf or CSV. Body: {"vcf": "..."} or {"csv": "..."}.""" - text = data.get("vcf") or data.get("text") or "" - csv_text = data.get("csv") or "" + # Coerce defensively: a non-string vcf/text/csv (e.g. a number or list + # in the JSON body) would otherwise reach .strip() and 500 with an + # AttributeError instead of degrading to a clean "no data" response. + text = str(data.get("vcf") or data.get("text") or "") + csv_text = str(data.get("csv") or "") if text.strip(): if "BEGIN:VCARD" not in text.upper(): return {"success": False, "error": "No vCard data found"} diff --git a/tests/test_contacts_import_nonstring.py b/tests/test_contacts_import_nonstring.py new file mode 100644 index 000000000..c029b569d --- /dev/null +++ b/tests/test_contacts_import_nonstring.py @@ -0,0 +1,39 @@ +"""POST /api/contacts/import must not 500 on a non-string vcf/text/csv value. + +`text = data.get("vcf") or ... or ""` left a non-string value (e.g. a number) +in place, so the next `text.strip()` raised AttributeError -> HTTP 500. The +handler now coerces with str() and degrades to a structured "no data" response. +""" +import asyncio + +from routes.contacts_routes import setup_contacts_routes + + +def _import_handler(): + router = setup_contacts_routes() + for route in router.routes: + if getattr(route, "path", "").endswith("/import") and "POST" in getattr(route, "methods", set()): + return route.endpoint + raise AssertionError("import route not found") + + +def _call(data): + handler = _import_handler() + return asyncio.run(handler(data=data, _admin="admin")) + + +def test_non_string_vcf_degrades_cleanly(): + resp = _call({"vcf": 123}) + assert resp["success"] is False + assert "error" in resp + + +def test_non_string_csv_degrades_cleanly(): + resp = _call({"csv": ["a", "b"]}) + assert resp["success"] is False + + +def test_empty_body_reports_no_data(): + resp = _call({}) + assert resp["success"] is False + assert resp["error"] == "No contact data found"