bottleneckcrm/backend/tasks/tests/test_custom_fields.py
Ashwin bd79a7a4cc Refactor code style and improve error handling in analytics API
- Cleaned up admin.py by removing unnecessary dictionary wrapping for fields.
- Adjusted migration files to maintain consistent formatting and removed redundant lines.
- Updated models.py and tests to remove unnecessary blank lines and improve readability.
- Enhanced error handling in the analytics API to properly forward upstream status and messages.
- Added comprehensive tests for the analytics proxy to ensure correct status and body forwarding.
- Updated contributing guidelines to clarify the use of ruff for code style enforcement.
2026-08-07 15:58:35 +05:30

240 lines
7.7 KiB
Python

"""Tasks-side tests for custom fields: Task create/update payloads,
list filter, and detail-page response shape.
Mirrors opportunity/tests/test_custom_fields.py.
"""
from __future__ import annotations
import pytest
from django.db import connection
from common.models import CustomFieldDefinition
from tasks.models import Task
pg_only = pytest.mark.skipif(
connection.vendor != "postgresql",
reason="JSONField __contains lookup requires PostgreSQL",
)
TASKS_LIST_URL = "/api/tasks/"
def _set_rls(org):
if connection.vendor != "postgresql":
return
with connection.cursor() as cursor:
cursor.execute("SELECT set_config('app.current_org', %s, false)", [str(org.id)])
def _make_complexity_def(org, **overrides):
defaults = {
"target_model": "Task",
"key": "complexity",
"label": "Complexity",
"field_type": "dropdown",
"options": [
{"value": "trivial", "label": "Trivial"},
{"value": "epic", "label": "Epic"},
],
"is_required": False,
"is_active": True,
}
defaults.update(overrides)
return CustomFieldDefinition.objects.create(org=org, **defaults)
@pytest.fixture
def task_a(org_a, admin_user):
_set_rls(org_a)
return Task.objects.create(
title="Primary Task",
status="New",
priority="Medium",
org=org_a,
created_by=admin_user,
)
@pytest.fixture
def task_a2(org_a, admin_user):
_set_rls(org_a)
return Task.objects.create(
title="Secondary Task",
status="New",
priority="Low",
org=org_a,
created_by=admin_user,
)
@pytest.mark.django_db
class TestTaskCreateWithCustomFields:
def test_create_with_valid_dropdown_value(self, admin_client, org_a):
_make_complexity_def(org_a)
response = admin_client.post(
TASKS_LIST_URL,
{
"title": "Tiered Task",
"status": "New",
"priority": "Low",
"custom_fields": {"complexity": "epic"},
},
format="json",
)
assert response.status_code == 200, response.content
task = Task.objects.get(title="Tiered Task", org=org_a)
assert task.custom_fields == {"complexity": "epic"}
def test_create_rejects_invalid_dropdown_value(self, admin_client, org_a):
_make_complexity_def(org_a)
response = admin_client.post(
TASKS_LIST_URL,
{
"title": "Bad Tier Task",
"status": "New",
"priority": "Low",
"custom_fields": {"complexity": "platinum"},
},
format="json",
)
assert response.status_code == 400
assert "custom_fields" in response.json()["errors"]
assert "complexity" in response.json()["errors"]["custom_fields"]
def test_create_drops_unknown_keys(self, admin_client, org_a):
_make_complexity_def(org_a)
response = admin_client.post(
TASKS_LIST_URL,
{
"title": "Unknown Key Task",
"status": "New",
"priority": "Low",
"custom_fields": {"complexity": "trivial", "made_up": "ignored"},
},
format="json",
)
assert response.status_code == 200
task = Task.objects.get(title="Unknown Key Task", org=org_a)
assert task.custom_fields == {"complexity": "trivial"}
def test_create_required_missing_returns_400(self, admin_client, org_a):
_make_complexity_def(org_a, is_required=True)
response = admin_client.post(
TASKS_LIST_URL,
{"title": "No CF Task", "status": "New", "priority": "Low"},
format="json",
)
assert response.status_code == 400
assert "complexity" in response.json()["errors"]["custom_fields"]
@pytest.mark.django_db
class TestTaskUpdateWithCustomFields:
def test_patch_merges_with_existing(self, admin_client, task_a, org_a):
_make_complexity_def(org_a)
_make_complexity_def(
org_a,
key="story_points",
field_type="number",
options=None,
)
task_a.custom_fields = {"complexity": "epic"}
task_a.save()
response = admin_client.patch(
f"{TASKS_LIST_URL}{task_a.id}/",
{"custom_fields": {"story_points": 13}},
format="json",
)
assert response.status_code == 200, response.content
task_a.refresh_from_db()
assert task_a.custom_fields == {
"complexity": "epic",
"story_points": 13.0,
}
def test_patch_invalid_returns_400(self, admin_client, task_a, org_a):
_make_complexity_def(org_a)
response = admin_client.patch(
f"{TASKS_LIST_URL}{task_a.id}/",
{"custom_fields": {"complexity": "platinum"}},
format="json",
)
assert response.status_code == 400
def test_put_replaces_custom_fields(self, admin_client, task_a, org_a):
_make_complexity_def(org_a)
task_a.custom_fields = {"complexity": "trivial"}
task_a.save()
response = admin_client.put(
f"{TASKS_LIST_URL}{task_a.id}/",
{
"title": task_a.title,
"status": task_a.status,
"priority": task_a.priority,
"custom_fields": {"complexity": "epic"},
},
format="json",
)
assert response.status_code == 200, response.content
task_a.refresh_from_db()
assert task_a.custom_fields == {"complexity": "epic"}
@pg_only
@pytest.mark.django_db
class TestTaskListFilter:
def test_cf_filter_returns_matching(self, admin_client, task_a, task_a2, org_a):
_make_complexity_def(org_a)
task_a.custom_fields = {"complexity": "epic"}
task_a.save()
task_a2.custom_fields = {"complexity": "trivial"}
task_a2.save()
response = admin_client.get(f"{TASKS_LIST_URL}?cf_complexity=epic")
body = response.json()
ids = [t["id"] for t in body["tasks"]]
assert str(task_a.id) in ids
assert str(task_a2.id) not in ids
@pytest.mark.django_db
class TestTaskDetailResponse:
def test_detail_includes_definitions_and_values(self, admin_client, task_a, org_a):
_make_complexity_def(org_a)
task_a.custom_fields = {"complexity": "trivial"}
task_a.save()
response = admin_client.get(f"{TASKS_LIST_URL}{task_a.id}/")
assert response.status_code == 200, response.content
body = response.json()
assert body["task_obj"]["custom_fields"] == {"complexity": "trivial"}
keys = [d["key"] for d in body["custom_field_definitions"]]
assert "complexity" in keys
def test_detail_excludes_inactive_definitions(self, admin_client, task_a, org_a):
_make_complexity_def(org_a)
_make_complexity_def(
org_a, key="legacy", field_type="text", options=None, is_active=False
)
response = admin_client.get(f"{TASKS_LIST_URL}{task_a.id}/")
keys = [d["key"] for d in response.json()["custom_field_definitions"]]
assert "complexity" in keys
assert "legacy" not in keys
def test_cross_org_does_not_leak_definitions(
self, admin_client, task_a, org_a, org_b
):
CustomFieldDefinition.objects.create(
org=org_b,
target_model="Task",
key="other_org_field",
label="Other org",
field_type="text",
)
response = admin_client.get(f"{TASKS_LIST_URL}{task_a.id}/")
keys = [d["key"] for d in response.json()["custom_field_definitions"]]
assert "other_org_field" not in keys