fix(contacts): persist all documented fields on create (fixes #716)

create_contact previously read only fn/email/tel from contact_data and
silently dropped org, organization, note, title, nickname, bday,
categories, url — and didn't accept phone as an alias for tel, so the
reporter's exact call lost every field except fn and email. Introduce
_build_contact_from_data, share it with update_contact's fallback, and
normalise str→list inputs so pythonvCard4 doesn't iterate bare strings
character-by-character for list-typed properties.

Closes #716

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-23 07:28:05 +02:00
co-authored by Claude Opus 4.7
parent d766f3c014
commit 723aee9134
4 changed files with 174 additions and 16 deletions
@@ -86,3 +86,47 @@ async def test_full_contact_workflow(
contacts = await nc_client.contacts.list_contacts(addressbook=addressbook_name)
contact_uids = [c["vcard_id"] for c in contacts]
assert contact_uid not in contact_uids
async def test_create_contact_persists_all_documented_fields(
nc_client: NextcloudClient, temporary_addressbook: str
):
"""Regression for issue #716: org/note/phone/organization must persist to the vCard.
Historically ``create_contact`` only handled fn/email/tel and silently dropped every
other key. Inspect the raw server-side vCard (not just the parsed list response) to
confirm each documented field round-trips.
"""
addressbook_name = temporary_addressbook
contact_uid = f"test-full-{uuid.uuid4().hex[:8]}"
contact_data = {
"fn": "Full Field User",
"email": "full@example.com",
"phone": "555-0716", # alias for tel
"organization": "Acme Corp", # alias for org
"note": "Issue 716 regression",
"title": "Engineer",
"url": "https://example.com",
}
await nc_client.contacts.create_contact(
addressbook=addressbook_name,
uid=contact_uid,
contact_data=contact_data,
)
try:
raw_vcard, _etag = await nc_client.contacts._get_raw_vcard(
addressbook_name, contact_uid
)
assert "FN:Full Field User" in raw_vcard
assert "EMAIL" in raw_vcard and "full@example.com" in raw_vcard
assert "TEL" in raw_vcard and "555-0716" in raw_vcard
assert "ORG:Acme Corp" in raw_vcard
assert "NOTE:Issue 716 regression" in raw_vcard
assert "TITLE:Engineer" in raw_vcard
# Sabre rewrites bare URL: to URL;VALUE=URI: on PUT
assert "URL" in raw_vcard and "https://example.com" in raw_vcard
finally:
await nc_client.contacts.delete_contact(
addressbook=addressbook_name, uid=contact_uid
)
+9 -2
View File
@@ -24,6 +24,9 @@ async def test_mcp_contacts_workflow(
"fn": f"MCP Contact {unique_suffix}",
"email": f"mcp.contact.{unique_suffix}@example.com",
"tel": "1234567890",
# Regression for issue #716 — these were silently dropped before
"organization": "MCP Test Corp",
"note": f"Created by test {unique_suffix}",
}
try:
@@ -51,9 +54,13 @@ async def test_mcp_contacts_workflow(
)
assert create_c_result.isError is False
# 4. Verify contact creation
# 4. Verify contact creation (and that all fields — #716 — actually persisted)
contacts = await nc_client.contacts.list_contacts(addressbook=addressbook_name)
assert any(c["vcard_id"] == contact_uid for c in contacts)
created = next((c for c in contacts if c["vcard_id"] == contact_uid), None)
assert created is not None
raw_vcard = created.get("addressdata", "")
assert "ORG:MCP Test Corp" in raw_vcard
assert f"NOTE:Created by test {unique_suffix}" in raw_vcard
# 5. Delete contact via MCP
logger.info(f"Deleting contact {contact_uid} via MCP")