165 lines
6.1 KiB
Python
165 lines
6.1 KiB
Python
|
|
"""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"
|