Files
leocrm/tests/test_custom_fields.py
Agent Zero db97a39133
Check Cross-Plugin Imports / check (push) Has been cancelled
feat(audit): P1 cross-tenant/RBAC tests, P3 test fixes, P2/P3 frontend fixes
- P1-Tests: 12 test files with new cross-tenant isolation + RBAC tests
- P3-Tests: 8 fixes (duplicate fixtures, sys.path.insert, unused imports, KeyError)
- P3-Frontend: LucideIcons → ICON_MAP (2 files), inline styles → Tailwind (2 files)
- P3-Frontend: DOMPurify for iframe XSS, redundant regex removed, console.log → console.debug
- P2-Frontend: 2 notification API TODOs retained (requires larger refactor)
- conftest.py: create_no_perm_user helper added
- pyproject.toml: pythonpath for scripts/ added
- All checks green: ruff 0, F821 0, tsc 0, app 495 routes, cross-plugin 0
2026-08-16 01:30:02 +02:00

212 lines
8.0 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"
# ── Cross-tenant isolation test ──
@pytest.mark.asyncio
class TestCustomFieldsCrossTenant:
"""Custom fields must not leak across tenants."""
async def test_cross_tenant_isolation(self, client: AsyncClient, db_session):
"""Tenant B cannot access custom fields of tenant A's contact."""
from httpx import ASGITransport
from httpx import AsyncClient as AC
import app.main
await seed_tenant_and_users(db_session)
await login_client(client, "admin@tenanta.com")
# Create a contact in tenant A
create_resp = await client.post(
"/api/v1/contacts",
json={"type": "company", "name": "Tenant A Custom Corp", "displayname": "Tenant A Custom Corp"},
headers=ORIGIN_HEADER,
)
assert create_resp.status_code == 201
contact_id = create_resp.json()["id"]
# Tenant B admin must not access tenant A's contact custom fields
app_instance = app.main.app
async with AC(transport=ASGITransport(app=app_instance), base_url="http://test") as client_b:
await login_client(client_b, "admin@tenantb.com")
resp = await client_b.get(
f"/api/v1/contacts/{contact_id}/custom-fields",
headers=ORIGIN_HEADER,
)
assert resp.status_code == 404
async def test_rbac_no_permission(self, client: AsyncClient, db_session):
"""User without contacts:read permission gets 403 on GET custom fields."""
from tests.conftest import create_no_perm_user
seed = await seed_tenant_and_users(db_session)
await create_no_perm_user(db_session, seed)
await login_client(client, "noperm@tenanta.com")
resp = await client.get(
"/api/v1/contacts/00000000-0000-0000-0000-000000000000/custom-fields",
headers=ORIGIN_HEADER,
)
assert resp.status_code == 403