servala-portal/src/tests/test_form_config.py

1067 lines
34 KiB
Python
Raw Normal View History

2025-11-06 14:53:58 +01:00
from unittest.mock import Mock
2025-11-06 15:06:10 +01:00
import jsonschema
from django.core.validators import MaxValueValidator, MinValueValidator
2025-11-06 14:53:58 +01:00
from django.db import models
from servala.core.crd import generate_custom_form_class
2025-12-04 17:18:57 +01:00
from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS
2025-11-06 15:06:10 +01:00
from servala.core.forms import ServiceDefinitionAdminForm
2025-11-06 14:53:58 +01:00
from servala.core.models import ControlPlaneCRD
def test_custom_model_form_class_returns_class_when_form_config_exists():
crd = Mock(spec=ControlPlaneCRD)
service_def = Mock()
service_def.form_config = {
"fieldsets": [
{
"title": "General",
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-06 14:53:58 +01:00
"required": True,
}
],
}
]
}
crd.service_definition = service_def
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
2025-11-06 14:53:58 +01:00
class Meta:
app_label = "test"
crd.django_model = TestModel
result = generate_custom_form_class(
crd.service_definition.form_config, crd.django_model
)
2025-11-06 14:53:58 +01:00
assert result is not None
assert hasattr(result, "form_config")
2025-11-06 15:06:10 +01:00
def test_form_config_schema_validates_minimal_config():
form = ServiceDefinitionAdminForm()
schema = form.form_config_schema
minimal_config = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Service Name",
"controlplane_field_mapping": "spec.serviceName",
}
]
}
]
}
jsonschema.validate(instance=minimal_config, schema=schema)
def test_form_config_schema_validates_config_with_null_integers():
form = ServiceDefinitionAdminForm()
schema = form.form_config_schema
config_with_nulls = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Service Name",
"controlplane_field_mapping": "spec.serviceName",
"max_length": None,
"required": False,
},
{
"type": "textarea",
"label": "Description",
"controlplane_field_mapping": "spec.description",
"rows": None,
"max_length": None,
},
{
"type": "number",
"label": "Port",
"controlplane_field_mapping": "spec.port",
"min_value": None,
"max_value": None,
},
{
"type": "array",
"label": "Tags",
"controlplane_field_mapping": "spec.tags",
"min_values": None,
"max_values": None,
},
]
}
]
}
jsonschema.validate(instance=config_with_nulls, schema=schema)
def test_form_config_schema_validates_full_config():
form = ServiceDefinitionAdminForm()
schema = form.form_config_schema
full_config = {
"fieldsets": [
{
"title": "Service Configuration",
"fields": [
{
"type": "text",
"label": "Service Name",
"controlplane_field_mapping": "spec.serviceName",
"help_text": "Enter a unique service name",
"required": True,
"max_length": 100,
},
{
"type": "email",
"label": "Admin Email",
"controlplane_field_mapping": "spec.adminEmail",
"help_text": "Contact email for service administrator",
"required": True,
"max_length": 255,
},
{
"type": "textarea",
"label": "Description",
"controlplane_field_mapping": "spec.description",
"help_text": "Describe the service purpose",
"required": False,
"rows": 5,
"max_length": 500,
},
{
"type": "number",
"label": "Port",
"controlplane_field_mapping": "spec.port",
"help_text": "Service port number",
"required": True,
"min_value": 1,
"max_value": 65535,
},
{
"type": "choice",
"label": "Environment",
"controlplane_field_mapping": "spec.environment",
"help_text": "Deployment environment",
"required": True,
"choices": [
["dev", "Development"],
["staging", "Staging"],
["prod", "Production"],
],
},
{
"type": "checkbox",
"label": "Enable Monitoring",
"controlplane_field_mapping": "spec.monitoring.enabled",
"help_text": "Enable service monitoring",
"required": False,
},
{
"type": "array",
"label": "Tags",
"controlplane_field_mapping": "spec.tags",
"help_text": "Service tags for organization",
"required": False,
"min_values": 0,
"max_values": 10,
},
],
}
]
}
jsonschema.validate(instance=full_config, schema=schema)
2025-11-06 15:55:27 +01:00
def test_choice_field_uses_custom_choices_from_form_config():
"""Test that choice fields use custom choices when provided in form_config"""
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
2025-11-06 15:55:27 +01:00
environment = models.CharField(
max_length=20,
choices=[
("dev", "Development"),
("staging", "Staging"),
("prod", "Production"),
("test", "Testing"),
],
)
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"title": "General",
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-06 15:55:27 +01:00
"required": True,
},
{
"type": "choice",
"label": "Environment",
"controlplane_field_mapping": "environment",
"required": True,
"choices": [["dev", "Development"], ["prod", "Production"]],
},
],
}
]
}
form_class = generate_custom_form_class(form_config, TestModel)
form = form_class()
environment_field = form.fields["environment"]
assert list(environment_field.choices) == [
("dev", "Development"),
("prod", "Production"),
]
assert hasattr(environment_field, "_controlplane_choices")
assert len(environment_field._controlplane_choices) == 5 # 4 choices + empty choice
def test_choice_field_uses_control_plane_choices_when_no_custom_choices():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
2025-11-06 15:55:27 +01:00
environment = models.CharField(
max_length=20,
choices=[
("dev", "Development"),
("staging", "Staging"),
("prod", "Production"),
],
)
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"title": "General",
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-06 15:55:27 +01:00
"required": True,
},
{
"type": "choice",
"label": "Environment",
"controlplane_field_mapping": "environment",
"required": True,
},
],
}
]
}
form_class = generate_custom_form_class(form_config, TestModel)
form = form_class()
environment_field = form.fields["environment"]
choices_list = list(environment_field.choices)
assert len(choices_list) == 4 # 3 choices + empty choice
assert ("dev", "Development") in choices_list
def test_choice_field_validates_against_control_plane_choices():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
2025-11-06 15:55:27 +01:00
environment = models.CharField(
max_length=20,
choices=[
("dev", "Development"),
("staging", "Staging"),
("prod", "Production"),
],
)
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"title": "General",
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-06 15:55:27 +01:00
"required": True,
},
{
"type": "choice",
"label": "Environment",
"controlplane_field_mapping": "environment",
"required": True,
"choices": [["dev", "Development"], ["prod", "Production"]],
},
],
}
]
}
form_class = generate_custom_form_class(form_config, TestModel)
2025-12-09 09:33:06 +01:00
form = form_class(data={"display_name": "test-service", "environment": "dev"})
2025-11-06 15:55:27 +01:00
form.fields["context"].required = False # Skip context validation
assert form.is_valid(), f"Form should be valid but has errors: {form.errors}"
2025-12-09 09:33:06 +01:00
form = form_class(data={"display_name": "test-service", "environment": "prod"})
2025-11-06 15:55:27 +01:00
form.fields["context"].required = False # Skip context validation
assert form.is_valid(), f"Form should be valid but has errors: {form.errors}"
2025-12-09 09:33:06 +01:00
form = form_class(data={"display_name": "test-service", "environment": "invalid"})
2025-11-06 15:55:27 +01:00
form.fields["context"].required = False # Skip context validation
assert not form.is_valid()
assert "environment" in form.errors
def test_admin_form_validates_choice_values_against_schema():
form = ServiceDefinitionAdminForm()
mock_crd = Mock()
mock_crd.resource_schema = {
"properties": {
"spec": {
"properties": {
"environment": {
"type": "string",
"enum": ["dev", "staging", "prod"],
}
}
}
}
}
valid_form_config = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-06 15:55:27 +01:00
},
{
"type": "choice",
"label": "Environment",
"controlplane_field_mapping": "spec.environment",
"choices": [["dev", "Development"], ["prod", "Production"]],
},
]
}
]
}
spec_schema = mock_crd.resource_schema["properties"]["spec"]
errors = []
for field in valid_form_config["fieldsets"][0]["fields"]:
if field.get("type") == "choice":
form._validate_choice_field(
field, field["controlplane_field_mapping"], spec_schema, "spec", errors
)
assert len(errors) == 0, f"Expected no errors but got: {errors}"
invalid_form_config = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-06 15:55:27 +01:00
},
{
"type": "choice",
"label": "Environment",
"controlplane_field_mapping": "spec.environment",
"choices": [
["dev", "Development"],
["invalid", "Invalid Environment"],
],
},
]
}
]
}
errors = []
for field in invalid_form_config["fieldsets"][0]["fields"]:
if field.get("type") == "choice":
form._validate_choice_field(
field, field["controlplane_field_mapping"], spec_schema, "spec", errors
)
assert len(errors) > 0, "Expected validation errors but got none"
error_message = str(errors[0])
assert "invalid" in error_message.lower()
assert "Environment" in error_message
def test_number_field_min_max_sets_widget_attributes():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
port = models.IntegerField()
replica_count = models.IntegerField()
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"title": "General",
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
"required": True,
},
{
"type": "number",
"label": "Port",
"controlplane_field_mapping": "port",
"required": True,
"min_value": 1,
"max_value": 65535,
},
{
"type": "number",
"label": "Replicas",
"controlplane_field_mapping": "replica_count",
"required": True,
"min_value": 1,
"max_value": 10,
},
],
}
]
}
form_class = generate_custom_form_class(form_config, TestModel)
form = form_class()
port_field = form.fields["port"]
assert port_field.widget.attrs.get("min") == 1
assert port_field.widget.attrs.get("max") == 65535
replica_field = form.fields["replica_count"]
assert replica_field.widget.attrs.get("min") == 1
assert replica_field.widget.attrs.get("max") == 10
port_validators = port_field.validators
assert any(
isinstance(v, MinValueValidator) and v.limit_value == 1 for v in port_validators
)
assert any(
isinstance(v, MaxValueValidator) and v.limit_value == 65535
for v in port_validators
)
def test_default_value_for_all_field_types():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
description = models.TextField()
port = models.IntegerField()
environment = models.CharField(
max_length=20,
choices=[
("dev", "Development"),
("staging", "Staging"),
("prod", "Production"),
],
)
monitoring_enabled = models.BooleanField()
tags = models.JSONField()
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
"default_value": "default-name",
},
{
"type": "textarea",
"label": "Description",
"controlplane_field_mapping": "description",
"default_value": "Default description text",
},
{
"type": "number",
"label": "Port",
"controlplane_field_mapping": "port",
"default_value": "8080",
},
{
"type": "choice",
"label": "Environment",
"controlplane_field_mapping": "environment",
"default_value": "dev",
},
{
"type": "checkbox",
"label": "Enable Monitoring",
"controlplane_field_mapping": "monitoring_enabled",
"default_value": "true",
},
{
"type": "array",
"label": "Tags",
"controlplane_field_mapping": "tags",
"default_value": "tag1,tag2,tag3",
},
],
}
]
}
form_class = generate_custom_form_class(form_config, TestModel)
form = form_class()
2025-12-09 09:33:06 +01:00
assert form.fields["display_name"].initial == "default-name"
assert form.fields["description"].initial == "Default description text"
assert form.fields["port"].initial == "8080"
assert form.fields["environment"].initial == "dev"
assert form.fields["monitoring_enabled"].initial == "true"
assert form.fields["tags"].initial == "tag1,tag2,tag3"
def test_default_value_not_override_existing_instance():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
port = models.IntegerField()
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
"default_value": "default-name",
},
{
"type": "number",
"label": "Port",
"controlplane_field_mapping": "port",
"default_value": "8080",
},
],
}
]
}
2025-12-09 09:33:06 +01:00
instance = TestModel(display_name="existing-name", port=3000)
form_class = generate_custom_form_class(form_config, TestModel)
form = form_class(instance=instance)
2025-12-09 09:33:06 +01:00
assert form.initial["display_name"] == "existing-name"
assert form.initial["port"] == 3000
def test_form_config_coerces_string_numbers_to_integers():
form = ServiceDefinitionAdminForm()
schema = form.form_config_schema
config_with_string_numbers = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Service Name",
"controlplane_field_mapping": "spec.serviceName",
"max_length": "64", # String instead of integer
"required": True,
},
{
"type": "textarea",
"label": "Description",
"controlplane_field_mapping": "spec.description",
"rows": "5", # String instead of integer
"max_length": "500", # String instead of integer
},
{
"type": "number",
"label": "Port",
"controlplane_field_mapping": "spec.port",
"min_value": "1", # String instead of integer
"max_value": "65535", # String instead of integer
},
{
"type": "array",
"label": "Tags",
"controlplane_field_mapping": "spec.tags",
"min_values": "0", # String instead of integer
"max_values": "10", # String instead of integer
},
]
}
]
}
normalized_config = form._normalize_form_config_types(config_with_string_numbers)
fields = normalized_config["fieldsets"][0]["fields"]
assert fields[0]["max_length"] == 64
assert isinstance(fields[0]["max_length"], int)
assert fields[1]["rows"] == 5
assert isinstance(fields[1]["rows"], int)
assert fields[1]["max_length"] == 500
assert isinstance(fields[1]["max_length"], int)
assert fields[2]["min_value"] == 1
assert isinstance(fields[2]["min_value"], int)
assert fields[2]["max_value"] == 65535
assert isinstance(fields[2]["max_value"], int)
assert fields[3]["min_values"] == 0
assert isinstance(fields[3]["min_values"], int)
assert fields[3]["max_values"] == 10
assert isinstance(fields[3]["max_values"], int)
jsonschema.validate(instance=normalized_config, schema=schema)
def test_form_config_handles_float_numbers():
form = ServiceDefinitionAdminForm()
config_with_floats = {
"fieldsets": [
{
"fields": [
{
"type": "number",
"label": "Price",
"controlplane_field_mapping": "spec.price",
"min_value": "0.01", # String float
"max_value": "999.99", # String float
},
]
}
]
}
normalized_config = form._normalize_form_config_types(config_with_floats)
field = normalized_config["fieldsets"][0]["fields"][0]
assert field["min_value"] == 0.01
assert isinstance(field["min_value"], float)
assert field["max_value"] == 999.99
assert isinstance(field["max_value"], float)
def test_form_config_handles_empty_string_as_none():
form = ServiceDefinitionAdminForm()
config_with_empty_strings = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
"max_length": "", # Empty string
},
]
}
]
}
normalized_config = form._normalize_form_config_types(config_with_empty_strings)
field = normalized_config["fieldsets"][0]["fields"][0]
assert field["max_length"] is None
def test_single_element_choices_are_normalized():
form = ServiceDefinitionAdminForm()
mock_crd = Mock()
mock_crd.resource_schema = {
"properties": {
"spec": {
"properties": {
"version": {
"type": "string",
"enum": ["6.2", "7.0", "7.2"],
}
}
}
}
}
config_with_single_choices = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
},
{
"type": "choice",
"label": "Version",
"controlplane_field_mapping": "spec.version",
"choices": [["6.2"]], # Single element - should be transformed
},
]
}
]
}
spec_schema = mock_crd.resource_schema["properties"]["spec"]
errors = []
for field in config_with_single_choices["fieldsets"][0]["fields"]:
if field.get("type") == "choice":
form._validate_choice_field(
field, field["controlplane_field_mapping"], spec_schema, "spec", errors
)
assert len(errors) == 0, f"Expected no errors but got: {errors}"
version_field = config_with_single_choices["fieldsets"][0]["fields"][1]
assert version_field["choices"] == [["6.2", "6.2"]]
def test_two_element_choices_work_correctly():
form = ServiceDefinitionAdminForm()
mock_crd = Mock()
mock_crd.resource_schema = {
"properties": {
"spec": {
"properties": {
"version": {
"type": "string",
"enum": ["6.2", "7.0"],
}
}
}
}
}
config_with_proper_choices = {
"fieldsets": [
{
"fields": [
{
"type": "choice",
"label": "Version",
"controlplane_field_mapping": "spec.version",
"choices": [["6.2", "Version 6.2"], ["7.0", "Version 7.0"]],
},
]
}
]
}
spec_schema = mock_crd.resource_schema["properties"]["spec"]
errors = []
for field in config_with_proper_choices["fieldsets"][0]["fields"]:
2025-12-04 17:18:57 +01:00
assert field["type"] == "choice"
form._validate_choice_field(
field, field["controlplane_field_mapping"], spec_schema, "spec", errors
)
assert len(errors) == 0, f"Expected no errors but got: {errors}"
version_field = config_with_proper_choices["fieldsets"][0]["fields"][0]
assert version_field["choices"] == [["6.2", "Version 6.2"], ["7.0", "Version 7.0"]]
def test_empty_choices_fail_validation():
form = ServiceDefinitionAdminForm()
config_with_empty_choice = {
"fieldsets": [
{
"fields": [
{
"type": "choice",
"label": "Version",
"controlplane_field_mapping": "spec.version",
"choices": [[]], # Empty choice - invalid
},
]
}
]
}
errors = []
for field in config_with_empty_choice["fieldsets"][0]["fields"]:
2025-12-04 17:18:57 +01:00
assert field["type"] == "choice"
form._validate_choice_field(
field, field["controlplane_field_mapping"], {}, "spec", errors
)
assert len(errors) > 0
assert "must have 1 or 2 elements" in str(errors[0])
def test_three_plus_element_choices_fail_validation():
form = ServiceDefinitionAdminForm()
config_with_long_choice = {
"fieldsets": [
{
"fields": [
{
"type": "choice",
"label": "Version",
"controlplane_field_mapping": "spec.version",
"choices": [
["6.2", "Version 6.2", "Extra"]
], # 3 elements - invalid
},
]
}
]
}
errors = []
for field in config_with_long_choice["fieldsets"][0]["fields"]:
2025-12-04 17:18:57 +01:00
assert field["type"] == "choice"
form._validate_choice_field(
field, field["controlplane_field_mapping"], {}, "spec", errors
)
assert len(errors) > 0
assert "must have 1 or 2 elements" in str(errors[0])
2025-11-10 14:02:34 +01:00
def test_field_with_default_config_only_needs_mapping():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
2025-11-10 14:02:34 +01:00
class Meta:
app_label = "test"
minimal_config = {
"fieldsets": [
{
"fields": [
{
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-10 14:02:34 +01:00
},
]
}
]
}
form_class = generate_custom_form_class(minimal_config, TestModel)
form = form_class()
2025-12-09 09:33:06 +01:00
name_field = form.fields["display_name"]
assert name_field.label == DEFAULT_FIELD_CONFIGS["display_name"]["label"]
assert name_field.help_text == DEFAULT_FIELD_CONFIGS["display_name"]["help_text"]
assert name_field.required == DEFAULT_FIELD_CONFIGS["display_name"]["required"]
2025-11-10 14:02:34 +01:00
def test_field_with_default_config_can_override_defaults():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
2025-11-10 14:02:34 +01:00
class Meta:
app_label = "test"
override_config = {
"fieldsets": [
{
"fields": [
{
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-10 14:02:34 +01:00
"label": "Custom Name Label",
"required": False,
},
]
}
]
}
form_class = generate_custom_form_class(override_config, TestModel)
form = form_class()
2025-12-09 09:33:06 +01:00
name_field = form.fields["display_name"]
2025-11-10 14:02:34 +01:00
assert name_field.label == "Custom Name Label"
assert name_field.required is False
2025-12-09 09:33:06 +01:00
assert name_field.help_text == DEFAULT_FIELD_CONFIGS["display_name"]["help_text"]
2025-11-10 14:02:34 +01:00
def test_empty_values_dont_override_default_configs():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
2025-11-10 14:02:34 +01:00
class Meta:
app_label = "test"
admin_form_config = {
"fieldsets": [
{
"fields": [
{
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
2025-11-10 14:02:34 +01:00
"type": "",
"label": "",
"help_text": None,
"max_length": None,
"required": False,
},
]
}
]
}
form_class = generate_custom_form_class(admin_form_config, TestModel)
form = form_class()
2025-12-09 09:33:06 +01:00
name_field = form.fields["display_name"]
2025-11-10 14:02:34 +01:00
2025-12-09 09:33:06 +01:00
assert name_field.label == DEFAULT_FIELD_CONFIGS["display_name"]["label"]
assert name_field.help_text == DEFAULT_FIELD_CONFIGS["display_name"]["help_text"]
assert name_field.max_length == DEFAULT_FIELD_CONFIGS["display_name"]["max_length"]
2025-11-10 14:02:34 +01:00
assert name_field.required is False # Was overridden by explicit False
def test_number_field_validates_min_max_values():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
port = models.IntegerField()
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"title": "General",
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
"required": True,
},
{
"type": "number",
"label": "Port",
"controlplane_field_mapping": "port",
"required": True,
"min_value": "1",
"max_value": "65535",
},
],
}
]
}
form_class = generate_custom_form_class(form_config, TestModel)
# Test value below minimum fails validation
2025-12-09 09:33:06 +01:00
form = form_class(data={"display_name": "test-service", "port": 0})
form.fields["context"].required = False
assert not form.is_valid()
assert "port" in form.errors
# Test value above maximum fails validation
2025-12-09 09:33:06 +01:00
form = form_class(data={"display_name": "test-service", "port": 65536})
form.fields["context"].required = False
assert not form.is_valid()
assert "port" in form.errors
# Test valid value passes validation
2025-12-09 09:33:06 +01:00
form = form_class(data={"display_name": "test-service", "port": 8080})
form.fields["context"].required = False
assert form.is_valid(), f"Form should be valid but has errors: {form.errors}"
def test_number_field_with_addon_text_roundtrip():
class TestModel(models.Model):
2025-12-09 09:33:06 +01:00
display_name = models.CharField(max_length=100)
disk_size = models.IntegerField()
class Meta:
app_label = "test"
form_config = {
"fieldsets": [
{
"fields": [
{
"type": "text",
"label": "Name",
2025-12-09 09:33:06 +01:00
"controlplane_field_mapping": "display_name",
"required": True,
},
{
"type": "number",
"label": "Disk Size",
"controlplane_field_mapping": "disk_size",
"addon_text": "Gi",
},
],
}
]
}
form_class = generate_custom_form_class(form_config, TestModel)
form = form_class(initial={"name": "test-instance", "disk_size": "25Gi"})
assert form.initial["disk_size"] == 25
2025-12-09 09:33:06 +01:00
form = form_class(data={"display_name": "test-instance", "disk_size": "25"})
form.fields["context"].required = False
assert form.is_valid(), f"Form should be valid but has errors: {form.errors}"
nested_data = form.get_nested_data()
assert nested_data["disk_size"] == "25Gi"