bottleneckcrm/backend/webforms/tests/test_dynamic_serializer.py
Ashwin e26ab380fc Add unit tests for ticket board provider, status settable, web forms, CSV import, deal aging badge, and tickets selection
- Implement comprehensive tests for the ticket board provider to validate kanban and pipeline modes, including ticket movement and error handling.
- Create tests for ticket status settable to ensure proper status selection logic.
- Enhance web form model tests to cover ticket-specific fields and submission behavior.
- Introduce CSV import sheet tests to verify file handling and error reporting.
- Add deal aging badge tests to confirm correct display of aging statuses.
- Implement tickets selection tests to ensure UI responsiveness and functionality at various screen sizes.
2026-09-26 16:53:22 +05:30

267 lines
9.9 KiB
Python

"""Validation for one form's shape.
A web form is filled in by a stranger, so every rule here is a security rule
rather than a convenience. The dynamic part matters because the schema is rows
in a table: a form that collects three fields must reject a fourth.
"""
import pytest
from common.models import CustomFieldDefinition
from webforms.dynamic_serializer import HONEYPOT_FIELD, build_serializer
from webforms.models import WebForm, WebFormField
@pytest.fixture
def form(org_a):
web_form = WebForm.objects.create(name="Contact us", org=org_a, is_published=True)
WebFormField.objects.create(
form=web_form,
org=org_a,
order=0,
source=WebFormField.SOURCE_LEAD,
lead_field="email",
label="Email",
is_required=True,
)
WebFormField.objects.create(
form=web_form,
org=org_a,
order=1,
source=WebFormField.SOURCE_LEAD,
lead_field="first_name",
label="First name",
)
return web_form
def budget_definition(org, is_required=False):
return CustomFieldDefinition.objects.create(
org=org,
target_model="Lead",
key="budget",
label="Budget",
field_type="number",
is_required=is_required,
)
def add_custom(form, org, definition, order=9):
return WebFormField.objects.create(
form=form,
org=org,
order=order,
source=WebFormField.SOURCE_CUSTOM,
custom_field=definition,
label=definition.label,
)
@pytest.mark.django_db
class TestAcceptance:
def test_a_valid_payload_passes(self, form):
serializer = build_serializer(form)(
data={"email": "pat@example.com", "first_name": "Pat"}
)
assert serializer.is_valid(), serializer.errors
def test_an_optional_field_may_be_omitted(self, form):
serializer = build_serializer(form)(data={"email": "pat@example.com"})
assert serializer.is_valid(), serializer.errors
def test_lead_values_are_keyed_by_lead_attribute_name(self, form):
serializer = build_serializer(form)(
data={"email": "pat@example.com", "first_name": "Pat"}
)
assert serializer.is_valid(), serializer.errors
assert serializer.field_values() == {
"email": "pat@example.com",
"first_name": "Pat",
}
def test_an_omitted_optional_field_is_absent_rather_than_blank(self, form):
serializer = build_serializer(form)(data={"email": "pat@example.com"})
assert serializer.is_valid(), serializer.errors
assert "first_name" not in serializer.field_values()
@pytest.mark.django_db
class TestRejection:
def test_a_missing_required_field_is_rejected(self, form):
serializer = build_serializer(form)(data={"first_name": "Pat"})
assert not serializer.is_valid()
assert "email" in serializer.errors
def test_a_malformed_email_is_rejected(self, form):
serializer = build_serializer(form)(data={"email": "not-an-email"})
assert not serializer.is_valid()
assert "email" in serializer.errors
def test_a_field_the_form_does_not_collect_is_dropped_not_stored(self, form):
"""An unlisted key must not reach the Lead. Dropping it rather than
answering 400 is deliberate: a 400 would tell a prober which Lead
columns exist."""
serializer = build_serializer(form)(
data={"email": "pat@example.com", "opportunity_amount": "999999"}
)
assert serializer.is_valid(), serializer.errors
assert "opportunity_amount" not in serializer.validated_data
assert "opportunity_amount" not in serializer.field_values()
def test_an_over_length_value_is_rejected(self, form, org_a):
WebFormField.objects.create(
form=form,
org=org_a,
order=2,
source=WebFormField.SOURCE_LEAD,
lead_field="phone",
label="Phone",
)
serializer = build_serializer(form)(
data={"email": "pat@example.com", "phone": "1" * 40}
)
assert not serializer.is_valid()
assert "phone" in serializer.errors
def test_a_disposable_domain_is_rejected_when_the_form_asks(self, form):
form.reject_disposable_email = True
form.save(update_fields=["reject_disposable_email"])
serializer = build_serializer(form)(data={"email": "pat@mailinator.com"})
assert not serializer.is_valid()
assert "email" in serializer.errors
def test_a_disposable_domain_is_accepted_when_the_form_does_not(self, form):
form.reject_disposable_email = False
form.save(update_fields=["reject_disposable_email"])
serializer = build_serializer(form)(data={"email": "pat@mailinator.com"})
assert serializer.is_valid(), serializer.errors
def test_a_value_outside_a_choice_field_is_rejected(self, form, org_a):
WebFormField.objects.create(
form=form,
org=org_a,
order=3,
source=WebFormField.SOURCE_LEAD,
lead_field="country",
label="Country",
)
serializer = build_serializer(form)(
data={"email": "pat@example.com", "country": "ZZZ"}
)
assert not serializer.is_valid()
assert "country" in serializer.errors
def test_a_valid_choice_value_passes(self, form, org_a):
WebFormField.objects.create(
form=form,
org=org_a,
order=3,
source=WebFormField.SOURCE_LEAD,
lead_field="country",
label="Country",
)
serializer = build_serializer(form)(
data={"email": "pat@example.com", "country": "IN"}
)
assert serializer.is_valid(), serializer.errors
@pytest.mark.django_db
class TestCustomFields:
def test_a_valid_custom_value_is_coerced(self, form, org_a):
add_custom(form, org_a, budget_definition(org_a))
serializer = build_serializer(form)(
data={"email": "pat@example.com", "budget": "5000"}
)
assert serializer.is_valid(), serializer.errors
assert serializer.custom_values() == {"budget": 5000.0}
def test_a_custom_value_of_the_wrong_type_is_rejected(self, form, org_a):
add_custom(form, org_a, budget_definition(org_a))
serializer = build_serializer(form)(
data={"email": "pat@example.com", "budget": "not a number"}
)
assert not serializer.is_valid()
assert "budget" in serializer.errors
def test_a_custom_value_never_leaks_into_lead_values(self, form, org_a):
add_custom(form, org_a, budget_definition(org_a))
serializer = build_serializer(form)(
data={"email": "pat@example.com", "budget": "5000"}
)
assert serializer.is_valid(), serializer.errors
assert "budget" not in serializer.field_values()
def test_a_dropdown_value_outside_its_options_is_rejected(self, form, org_a):
definition = CustomFieldDefinition.objects.create(
org=org_a,
target_model="Lead",
key="plan",
label="Plan",
field_type="dropdown",
options=[{"value": "pro", "label": "Pro"}],
)
add_custom(form, org_a, definition)
serializer = build_serializer(form)(
data={"email": "pat@example.com", "plan": "enterprise"}
)
assert not serializer.is_valid()
assert "plan" in serializer.errors
def test_a_required_definition_the_form_does_not_collect_does_not_block(
self, form, org_a
):
"""`common.custom_fields.validate_payload` errors on every required
definition that is absent, including ones this form has no input for.
Taken literally that would make a single required Lead custom field
break every web form in the org, losing the lead entirely. The form's
own field list governs here."""
budget_definition(org_a, is_required=True)
serializer = build_serializer(form)(data={"email": "pat@example.com"})
assert serializer.is_valid(), serializer.errors
assert serializer.custom_values() == {}
def test_a_required_definition_the_form_does_collect_still_blocks(
self, form, org_a
):
definition = budget_definition(org_a, is_required=True)
row = add_custom(form, org_a, definition)
row.is_required = True
row.save(update_fields=["is_required"])
serializer = build_serializer(form)(data={"email": "pat@example.com"})
assert not serializer.is_valid()
assert "budget" in serializer.errors
@pytest.mark.django_db
class TestHoneypot:
def test_the_honeypot_field_is_always_present(self, form):
assert HONEYPOT_FIELD in build_serializer(form)().fields
def test_an_empty_honeypot_passes(self, form):
serializer = build_serializer(form)(
data={"email": "pat@example.com", HONEYPOT_FIELD: ""}
)
assert serializer.is_valid(), serializer.errors
assert serializer.honeypot_tripped() is False
def test_a_filled_honeypot_is_detected(self, form):
serializer = build_serializer(form)(
data={"email": "pat@example.com", HONEYPOT_FIELD: "http://spam.example"}
)
assert serializer.is_valid(), serializer.errors
assert serializer.honeypot_tripped() is True
def test_a_whitespace_only_honeypot_is_not_tripped(self, form):
serializer = build_serializer(form)(
data={"email": "pat@example.com", HONEYPOT_FIELD: " "}
)
assert serializer.is_valid(), serializer.errors
assert serializer.honeypot_tripped() is False
def test_the_honeypot_never_reaches_the_lead_values(self, form):
serializer = build_serializer(form)(
data={"email": "pat@example.com", HONEYPOT_FIELD: "x"}
)
assert serializer.is_valid(), serializer.errors
assert HONEYPOT_FIELD not in serializer.field_values()