"""Custom fields tests — plugin-defined custom fields on contacts.""" from __future__ import annotations import pytest from httpx import AsyncClient from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users @pytest.mark.asyncio class TestCustomFieldsGet: """GET /api/v1/contacts/{id}/custom-fields""" async def test_get_custom_fields_returns_200(self, client: AsyncClient, db_session): """GET custom fields for a contact returns 200 with merged definitions.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") # Create a contact first resp = await client.post( "/api/v1/contacts", json={"type": "company", "name": "Test Corp", "displayname": "Test Corp"}, headers=ORIGIN_HEADER, ) assert resp.status_code == 201 contact_id = resp.json()["id"] resp = await client.get( f"/api/v1/contacts/{contact_id}/custom-fields", headers=ORIGIN_HEADER, ) assert resp.status_code == 200 data = resp.json() assert "fields" in data assert isinstance(data["fields"], list) async def test_get_custom_fields_invalid_uuid_returns_400(self, client: AsyncClient, db_session): """GET custom fields with invalid UUID returns 400.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.get( "/api/v1/contacts/not-a-uuid/custom-fields", headers=ORIGIN_HEADER, ) assert resp.status_code == 400 async def test_get_custom_fields_not_found_returns_404(self, client: AsyncClient, db_session): """GET custom fields for non-existent contact returns 404.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.get( "/api/v1/contacts/00000000-0000-0000-0000-000000000000/custom-fields", headers=ORIGIN_HEADER, ) assert resp.status_code == 404 @pytest.mark.asyncio class TestCustomFieldsUpdate: """PATCH /api/v1/contacts/{id}/custom-fields""" async def test_update_custom_fields_returns_200(self, client: AsyncClient, db_session): """PATCH custom fields stores values in contacts.custom JSONB.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") # Create a contact resp = await client.post( "/api/v1/contacts", json={"type": "company", "name": "Custom Corp", "displayname": "Custom Corp"}, headers=ORIGIN_HEADER, ) contact_id = resp.json()["id"] # Update custom fields (empty values is valid since no plugin defines fields) resp = await client.patch( f"/api/v1/contacts/{contact_id}/custom-fields", json={"values": {"test_field": "test_value"}}, headers=ORIGIN_HEADER, ) # Without plugin definitions, unknown fields should be rejected assert resp.status_code == 400 async def test_update_custom_fields_empty_values_returns_200(self, client: AsyncClient, db_session): """PATCH custom fields with empty values dict returns 200.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.post( "/api/v1/contacts", json={"type": "company", "name": "Empty Corp", "displayname": "Empty Corp"}, headers=ORIGIN_HEADER, ) contact_id = resp.json()["id"] resp = await client.patch( f"/api/v1/contacts/{contact_id}/custom-fields", json={"values": {}}, headers=ORIGIN_HEADER, ) assert resp.status_code == 200 assert "fields" in resp.json() async def test_update_custom_fields_not_found_returns_404(self, client: AsyncClient, db_session): """PATCH custom fields for non-existent contact returns 404.""" await seed_tenant_and_users(db_session) await login_client(client, "admin@tenanta.com") resp = await client.patch( "/api/v1/contacts/00000000-0000-0000-0000-000000000000/custom-fields", json={"values": {}}, headers=ORIGIN_HEADER, ) assert resp.status_code == 404 @pytest.mark.asyncio class TestCustomFieldDefinition: """Test CustomFieldDefinition model in manifest.""" async def test_custom_field_definition_creation(self): """CustomFieldDefinition can be created with valid field types.""" from app.plugins.manifest import CustomFieldDefinition cf = CustomFieldDefinition( name="lead_source", label="Lead Source", label_key="custom.leadSource", field_type="select", options=["website", "referral", "cold_call"], required=False, entity="contact", ) assert cf.name == "lead_source" assert cf.field_type == "select" assert len(cf.options) == 3 async def test_custom_field_definition_invalid_type_raises(self): """CustomFieldDefinition rejects invalid field_type.""" from app.plugins.manifest import CustomFieldDefinition from pydantic import ValidationError with pytest.raises(ValidationError): CustomFieldDefinition( name="bad_field", field_type="invalid_type", ) async def test_plugin_manifest_with_custom_fields(self): """PluginManifest accepts custom_fields list.""" from app.plugins.manifest import PluginManifest, CustomFieldDefinition manifest = PluginManifest( name="test_plugin", version="1.0.0", display_name="Test Plugin", custom_fields=[ CustomFieldDefinition( name="score", label="Score", field_type="number", entity="contact", ), ], ) assert len(manifest.custom_fields) == 1 assert manifest.custom_fields[0].name == "score"