fix: resolve AppCard description overlap with tag area (#23585)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -28,6 +28,12 @@ from services.feature_service import FeatureService
|
|||||||
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_description_length(description):
|
||||||
|
if description and len(description) > 400:
|
||||||
|
raise ValueError("Description cannot exceed 400 characters.")
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
class AppListApi(Resource):
|
class AppListApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@@ -94,7 +100,7 @@ class AppListApi(Resource):
|
|||||||
"""Create app"""
|
"""Create app"""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("name", type=str, required=True, location="json")
|
parser.add_argument("name", type=str, required=True, location="json")
|
||||||
parser.add_argument("description", type=str, location="json")
|
parser.add_argument("description", type=_validate_description_length, location="json")
|
||||||
parser.add_argument("mode", type=str, choices=ALLOW_CREATE_APP_MODES, location="json")
|
parser.add_argument("mode", type=str, choices=ALLOW_CREATE_APP_MODES, location="json")
|
||||||
parser.add_argument("icon_type", type=str, location="json")
|
parser.add_argument("icon_type", type=str, location="json")
|
||||||
parser.add_argument("icon", type=str, location="json")
|
parser.add_argument("icon", type=str, location="json")
|
||||||
@@ -146,7 +152,7 @@ class AppApi(Resource):
|
|||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("name", type=str, required=True, nullable=False, location="json")
|
parser.add_argument("name", type=str, required=True, nullable=False, location="json")
|
||||||
parser.add_argument("description", type=str, location="json")
|
parser.add_argument("description", type=_validate_description_length, location="json")
|
||||||
parser.add_argument("icon_type", type=str, location="json")
|
parser.add_argument("icon_type", type=str, location="json")
|
||||||
parser.add_argument("icon", type=str, location="json")
|
parser.add_argument("icon", type=str, location="json")
|
||||||
parser.add_argument("icon_background", type=str, location="json")
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
@@ -189,7 +195,7 @@ class AppCopyApi(Resource):
|
|||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("name", type=str, location="json")
|
parser.add_argument("name", type=str, location="json")
|
||||||
parser.add_argument("description", type=str, location="json")
|
parser.add_argument("description", type=_validate_description_length, location="json")
|
||||||
parser.add_argument("icon_type", type=str, location="json")
|
parser.add_argument("icon_type", type=str, location="json")
|
||||||
parser.add_argument("icon", type=str, location="json")
|
parser.add_argument("icon", type=str, location="json")
|
||||||
parser.add_argument("icon_background", type=str, location="json")
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
|
@@ -41,7 +41,7 @@ def _validate_name(name):
|
|||||||
|
|
||||||
|
|
||||||
def _validate_description_length(description):
|
def _validate_description_length(description):
|
||||||
if len(description) > 400:
|
if description and len(description) > 400:
|
||||||
raise ValueError("Description cannot exceed 400 characters.")
|
raise ValueError("Description cannot exceed 400 characters.")
|
||||||
return description
|
return description
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class DatasetListApi(Resource):
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"description",
|
"description",
|
||||||
type=str,
|
type=_validate_description_length,
|
||||||
nullable=True,
|
nullable=True,
|
||||||
required=False,
|
required=False,
|
||||||
default="",
|
default="",
|
||||||
|
@@ -29,7 +29,7 @@ def _validate_name(name):
|
|||||||
|
|
||||||
|
|
||||||
def _validate_description_length(description):
|
def _validate_description_length(description):
|
||||||
if len(description) > 400:
|
if description and len(description) > 400:
|
||||||
raise ValueError("Description cannot exceed 400 characters.")
|
raise ValueError("Description cannot exceed 400 characters.")
|
||||||
return description
|
return description
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class DatasetListApi(DatasetApiResource):
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"description",
|
"description",
|
||||||
type=str,
|
type=_validate_description_length,
|
||||||
nullable=True,
|
nullable=True,
|
||||||
required=False,
|
required=False,
|
||||||
default="",
|
default="",
|
||||||
|
@@ -0,0 +1,168 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for App description validation functions.
|
||||||
|
|
||||||
|
This test module validates the 400-character limit enforcement
|
||||||
|
for App descriptions across all creation and editing endpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Add the API root to Python path for imports
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
|
||||||
|
|
||||||
|
|
||||||
|
class TestAppDescriptionValidationUnit:
|
||||||
|
"""Unit tests for description validation function"""
|
||||||
|
|
||||||
|
def test_validate_description_length_function(self):
|
||||||
|
"""Test the _validate_description_length function directly"""
|
||||||
|
from controllers.console.app.app import _validate_description_length
|
||||||
|
|
||||||
|
# Test valid descriptions
|
||||||
|
assert _validate_description_length("") == ""
|
||||||
|
assert _validate_description_length("x" * 400) == "x" * 400
|
||||||
|
assert _validate_description_length(None) is None
|
||||||
|
|
||||||
|
# Test invalid descriptions
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
_validate_description_length("x" * 401)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
_validate_description_length("x" * 500)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
_validate_description_length("x" * 1000)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_validation_consistency_with_dataset(self):
|
||||||
|
"""Test that App and Dataset validation functions are consistent"""
|
||||||
|
from controllers.console.app.app import _validate_description_length as app_validate
|
||||||
|
from controllers.console.datasets.datasets import _validate_description_length as dataset_validate
|
||||||
|
from controllers.service_api.dataset.dataset import _validate_description_length as service_dataset_validate
|
||||||
|
|
||||||
|
# Test same valid inputs
|
||||||
|
valid_desc = "x" * 400
|
||||||
|
assert app_validate(valid_desc) == dataset_validate(valid_desc) == service_dataset_validate(valid_desc)
|
||||||
|
assert app_validate("") == dataset_validate("") == service_dataset_validate("")
|
||||||
|
assert app_validate(None) == dataset_validate(None) == service_dataset_validate(None)
|
||||||
|
|
||||||
|
# Test same invalid inputs produce same error
|
||||||
|
invalid_desc = "x" * 401
|
||||||
|
|
||||||
|
app_error = None
|
||||||
|
dataset_error = None
|
||||||
|
service_dataset_error = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_validate(invalid_desc)
|
||||||
|
except ValueError as e:
|
||||||
|
app_error = str(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dataset_validate(invalid_desc)
|
||||||
|
except ValueError as e:
|
||||||
|
dataset_error = str(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
service_dataset_validate(invalid_desc)
|
||||||
|
except ValueError as e:
|
||||||
|
service_dataset_error = str(e)
|
||||||
|
|
||||||
|
assert app_error == dataset_error == service_dataset_error
|
||||||
|
assert app_error == "Description cannot exceed 400 characters."
|
||||||
|
|
||||||
|
def test_boundary_values(self):
|
||||||
|
"""Test boundary values for description validation"""
|
||||||
|
from controllers.console.app.app import _validate_description_length
|
||||||
|
|
||||||
|
# Test exact boundary
|
||||||
|
exactly_400 = "x" * 400
|
||||||
|
assert _validate_description_length(exactly_400) == exactly_400
|
||||||
|
|
||||||
|
# Test just over boundary
|
||||||
|
just_over_400 = "x" * 401
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_validate_description_length(just_over_400)
|
||||||
|
|
||||||
|
# Test just under boundary
|
||||||
|
just_under_400 = "x" * 399
|
||||||
|
assert _validate_description_length(just_under_400) == just_under_400
|
||||||
|
|
||||||
|
def test_edge_cases(self):
|
||||||
|
"""Test edge cases for description validation"""
|
||||||
|
from controllers.console.app.app import _validate_description_length
|
||||||
|
|
||||||
|
# Test None input
|
||||||
|
assert _validate_description_length(None) is None
|
||||||
|
|
||||||
|
# Test empty string
|
||||||
|
assert _validate_description_length("") == ""
|
||||||
|
|
||||||
|
# Test single character
|
||||||
|
assert _validate_description_length("a") == "a"
|
||||||
|
|
||||||
|
# Test unicode characters
|
||||||
|
unicode_desc = "测试" * 200 # 400 characters in Chinese
|
||||||
|
assert _validate_description_length(unicode_desc) == unicode_desc
|
||||||
|
|
||||||
|
# Test unicode over limit
|
||||||
|
unicode_over = "测试" * 201 # 402 characters
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_validate_description_length(unicode_over)
|
||||||
|
|
||||||
|
def test_whitespace_handling(self):
|
||||||
|
"""Test how validation handles whitespace"""
|
||||||
|
from controllers.console.app.app import _validate_description_length
|
||||||
|
|
||||||
|
# Test description with spaces
|
||||||
|
spaces_400 = " " * 400
|
||||||
|
assert _validate_description_length(spaces_400) == spaces_400
|
||||||
|
|
||||||
|
# Test description with spaces over limit
|
||||||
|
spaces_401 = " " * 401
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_validate_description_length(spaces_401)
|
||||||
|
|
||||||
|
# Test mixed content
|
||||||
|
mixed_400 = "a" * 200 + " " * 200
|
||||||
|
assert _validate_description_length(mixed_400) == mixed_400
|
||||||
|
|
||||||
|
# Test mixed over limit
|
||||||
|
mixed_401 = "a" * 200 + " " * 201
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_validate_description_length(mixed_401)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run tests directly
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
test_instance = TestAppDescriptionValidationUnit()
|
||||||
|
test_methods = [method for method in dir(test_instance) if method.startswith("test_")]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for test_method in test_methods:
|
||||||
|
try:
|
||||||
|
print(f"Running {test_method}...")
|
||||||
|
getattr(test_instance, test_method)()
|
||||||
|
print(f"✅ {test_method} PASSED")
|
||||||
|
passed += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {test_method} FAILED: {str(e)}")
|
||||||
|
traceback.print_exc()
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\n📊 Test Results: {passed} passed, {failed} failed")
|
||||||
|
|
||||||
|
if failed == 0:
|
||||||
|
print("🎉 All tests passed!")
|
||||||
|
else:
|
||||||
|
print("💥 Some tests failed!")
|
||||||
|
sys.exit(1)
|
@@ -0,0 +1,252 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from controllers.console.app.app import _validate_description_length as app_validate
|
||||||
|
from controllers.console.datasets.datasets import _validate_description_length as dataset_validate
|
||||||
|
from controllers.service_api.dataset.dataset import _validate_description_length as service_dataset_validate
|
||||||
|
|
||||||
|
|
||||||
|
class TestDescriptionValidationUnit:
|
||||||
|
"""Unit tests for description validation functions in App and Dataset APIs"""
|
||||||
|
|
||||||
|
def test_app_validate_description_length_valid(self):
|
||||||
|
"""Test App validation function with valid descriptions"""
|
||||||
|
# Empty string should be valid
|
||||||
|
assert app_validate("") == ""
|
||||||
|
|
||||||
|
# None should be valid
|
||||||
|
assert app_validate(None) is None
|
||||||
|
|
||||||
|
# Short description should be valid
|
||||||
|
short_desc = "Short description"
|
||||||
|
assert app_validate(short_desc) == short_desc
|
||||||
|
|
||||||
|
# Exactly 400 characters should be valid
|
||||||
|
exactly_400 = "x" * 400
|
||||||
|
assert app_validate(exactly_400) == exactly_400
|
||||||
|
|
||||||
|
# Just under limit should be valid
|
||||||
|
just_under = "x" * 399
|
||||||
|
assert app_validate(just_under) == just_under
|
||||||
|
|
||||||
|
def test_app_validate_description_length_invalid(self):
|
||||||
|
"""Test App validation function with invalid descriptions"""
|
||||||
|
# 401 characters should fail
|
||||||
|
just_over = "x" * 401
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
app_validate(just_over)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
# 500 characters should fail
|
||||||
|
way_over = "x" * 500
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
app_validate(way_over)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
# 1000 characters should fail
|
||||||
|
very_long = "x" * 1000
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
app_validate(very_long)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_dataset_validate_description_length_valid(self):
|
||||||
|
"""Test Dataset validation function with valid descriptions"""
|
||||||
|
# Empty string should be valid
|
||||||
|
assert dataset_validate("") == ""
|
||||||
|
|
||||||
|
# Short description should be valid
|
||||||
|
short_desc = "Short description"
|
||||||
|
assert dataset_validate(short_desc) == short_desc
|
||||||
|
|
||||||
|
# Exactly 400 characters should be valid
|
||||||
|
exactly_400 = "x" * 400
|
||||||
|
assert dataset_validate(exactly_400) == exactly_400
|
||||||
|
|
||||||
|
# Just under limit should be valid
|
||||||
|
just_under = "x" * 399
|
||||||
|
assert dataset_validate(just_under) == just_under
|
||||||
|
|
||||||
|
def test_dataset_validate_description_length_invalid(self):
|
||||||
|
"""Test Dataset validation function with invalid descriptions"""
|
||||||
|
# 401 characters should fail
|
||||||
|
just_over = "x" * 401
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
dataset_validate(just_over)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
# 500 characters should fail
|
||||||
|
way_over = "x" * 500
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
dataset_validate(way_over)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_service_dataset_validate_description_length_valid(self):
|
||||||
|
"""Test Service Dataset validation function with valid descriptions"""
|
||||||
|
# Empty string should be valid
|
||||||
|
assert service_dataset_validate("") == ""
|
||||||
|
|
||||||
|
# None should be valid
|
||||||
|
assert service_dataset_validate(None) is None
|
||||||
|
|
||||||
|
# Short description should be valid
|
||||||
|
short_desc = "Short description"
|
||||||
|
assert service_dataset_validate(short_desc) == short_desc
|
||||||
|
|
||||||
|
# Exactly 400 characters should be valid
|
||||||
|
exactly_400 = "x" * 400
|
||||||
|
assert service_dataset_validate(exactly_400) == exactly_400
|
||||||
|
|
||||||
|
# Just under limit should be valid
|
||||||
|
just_under = "x" * 399
|
||||||
|
assert service_dataset_validate(just_under) == just_under
|
||||||
|
|
||||||
|
def test_service_dataset_validate_description_length_invalid(self):
|
||||||
|
"""Test Service Dataset validation function with invalid descriptions"""
|
||||||
|
# 401 characters should fail
|
||||||
|
just_over = "x" * 401
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
service_dataset_validate(just_over)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
# 500 characters should fail
|
||||||
|
way_over = "x" * 500
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
service_dataset_validate(way_over)
|
||||||
|
assert "Description cannot exceed 400 characters." in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_app_dataset_validation_consistency(self):
|
||||||
|
"""Test that App and Dataset validation functions behave identically"""
|
||||||
|
test_cases = [
|
||||||
|
"", # Empty string
|
||||||
|
"Short description", # Normal description
|
||||||
|
"x" * 100, # Medium description
|
||||||
|
"x" * 400, # Exactly at limit
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test valid cases produce same results
|
||||||
|
for test_desc in test_cases:
|
||||||
|
assert app_validate(test_desc) == dataset_validate(test_desc) == service_dataset_validate(test_desc)
|
||||||
|
|
||||||
|
# Test invalid cases produce same errors
|
||||||
|
invalid_cases = [
|
||||||
|
"x" * 401, # Just over limit
|
||||||
|
"x" * 500, # Way over limit
|
||||||
|
"x" * 1000, # Very long
|
||||||
|
]
|
||||||
|
|
||||||
|
for invalid_desc in invalid_cases:
|
||||||
|
app_error = None
|
||||||
|
dataset_error = None
|
||||||
|
service_dataset_error = None
|
||||||
|
|
||||||
|
# Capture App validation error
|
||||||
|
try:
|
||||||
|
app_validate(invalid_desc)
|
||||||
|
except ValueError as e:
|
||||||
|
app_error = str(e)
|
||||||
|
|
||||||
|
# Capture Dataset validation error
|
||||||
|
try:
|
||||||
|
dataset_validate(invalid_desc)
|
||||||
|
except ValueError as e:
|
||||||
|
dataset_error = str(e)
|
||||||
|
|
||||||
|
# Capture Service Dataset validation error
|
||||||
|
try:
|
||||||
|
service_dataset_validate(invalid_desc)
|
||||||
|
except ValueError as e:
|
||||||
|
service_dataset_error = str(e)
|
||||||
|
|
||||||
|
# All should produce errors
|
||||||
|
assert app_error is not None, f"App validation should fail for {len(invalid_desc)} characters"
|
||||||
|
assert dataset_error is not None, f"Dataset validation should fail for {len(invalid_desc)} characters"
|
||||||
|
error_msg = f"Service Dataset validation should fail for {len(invalid_desc)} characters"
|
||||||
|
assert service_dataset_error is not None, error_msg
|
||||||
|
|
||||||
|
# Errors should be identical
|
||||||
|
error_msg = f"Error messages should be identical for {len(invalid_desc)} characters"
|
||||||
|
assert app_error == dataset_error == service_dataset_error, error_msg
|
||||||
|
assert app_error == "Description cannot exceed 400 characters."
|
||||||
|
|
||||||
|
def test_boundary_values(self):
|
||||||
|
"""Test boundary values around the 400 character limit"""
|
||||||
|
boundary_tests = [
|
||||||
|
(0, True), # Empty
|
||||||
|
(1, True), # Minimum
|
||||||
|
(399, True), # Just under limit
|
||||||
|
(400, True), # Exactly at limit
|
||||||
|
(401, False), # Just over limit
|
||||||
|
(402, False), # Over limit
|
||||||
|
(500, False), # Way over limit
|
||||||
|
]
|
||||||
|
|
||||||
|
for length, should_pass in boundary_tests:
|
||||||
|
test_desc = "x" * length
|
||||||
|
|
||||||
|
if should_pass:
|
||||||
|
# Should not raise exception
|
||||||
|
assert app_validate(test_desc) == test_desc
|
||||||
|
assert dataset_validate(test_desc) == test_desc
|
||||||
|
assert service_dataset_validate(test_desc) == test_desc
|
||||||
|
else:
|
||||||
|
# Should raise ValueError
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
app_validate(test_desc)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
dataset_validate(test_desc)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
service_dataset_validate(test_desc)
|
||||||
|
|
||||||
|
def test_special_characters(self):
|
||||||
|
"""Test validation with special characters, Unicode, etc."""
|
||||||
|
# Unicode characters
|
||||||
|
unicode_desc = "测试描述" * 100 # Chinese characters
|
||||||
|
if len(unicode_desc) <= 400:
|
||||||
|
assert app_validate(unicode_desc) == unicode_desc
|
||||||
|
assert dataset_validate(unicode_desc) == unicode_desc
|
||||||
|
assert service_dataset_validate(unicode_desc) == unicode_desc
|
||||||
|
|
||||||
|
# Special characters
|
||||||
|
special_desc = "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?" * 10
|
||||||
|
if len(special_desc) <= 400:
|
||||||
|
assert app_validate(special_desc) == special_desc
|
||||||
|
assert dataset_validate(special_desc) == special_desc
|
||||||
|
assert service_dataset_validate(special_desc) == special_desc
|
||||||
|
|
||||||
|
# Mixed content
|
||||||
|
mixed_desc = "Mixed content: 测试 123 !@# " * 15
|
||||||
|
if len(mixed_desc) <= 400:
|
||||||
|
assert app_validate(mixed_desc) == mixed_desc
|
||||||
|
assert dataset_validate(mixed_desc) == mixed_desc
|
||||||
|
assert service_dataset_validate(mixed_desc) == mixed_desc
|
||||||
|
elif len(mixed_desc) > 400:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
app_validate(mixed_desc)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
dataset_validate(mixed_desc)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
service_dataset_validate(mixed_desc)
|
||||||
|
|
||||||
|
def test_whitespace_handling(self):
|
||||||
|
"""Test validation with various whitespace scenarios"""
|
||||||
|
# Leading/trailing whitespace
|
||||||
|
whitespace_desc = " Description with whitespace "
|
||||||
|
if len(whitespace_desc) <= 400:
|
||||||
|
assert app_validate(whitespace_desc) == whitespace_desc
|
||||||
|
assert dataset_validate(whitespace_desc) == whitespace_desc
|
||||||
|
assert service_dataset_validate(whitespace_desc) == whitespace_desc
|
||||||
|
|
||||||
|
# Newlines and tabs
|
||||||
|
multiline_desc = "Line 1\nLine 2\tTabbed content"
|
||||||
|
if len(multiline_desc) <= 400:
|
||||||
|
assert app_validate(multiline_desc) == multiline_desc
|
||||||
|
assert dataset_validate(multiline_desc) == multiline_desc
|
||||||
|
assert service_dataset_validate(multiline_desc) == multiline_desc
|
||||||
|
|
||||||
|
# Only whitespace over limit
|
||||||
|
only_spaces = " " * 401
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
app_validate(only_spaces)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
dataset_validate(only_spaces)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
service_dataset_validate(only_spaces)
|
97
web/__tests__/description-validation.test.tsx
Normal file
97
web/__tests__/description-validation.test.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Description Validation Test
|
||||||
|
*
|
||||||
|
* Tests for the 400-character description validation across App and Dataset
|
||||||
|
* creation and editing workflows to ensure consistent validation behavior.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('Description Validation Logic', () => {
|
||||||
|
// Simulate backend validation function
|
||||||
|
const validateDescriptionLength = (description?: string | null) => {
|
||||||
|
if (description && description.length > 400)
|
||||||
|
throw new Error('Description cannot exceed 400 characters.')
|
||||||
|
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Backend Validation Function', () => {
|
||||||
|
test('allows description within 400 characters', () => {
|
||||||
|
const validDescription = 'x'.repeat(400)
|
||||||
|
expect(() => validateDescriptionLength(validDescription)).not.toThrow()
|
||||||
|
expect(validateDescriptionLength(validDescription)).toBe(validDescription)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows empty description', () => {
|
||||||
|
expect(() => validateDescriptionLength('')).not.toThrow()
|
||||||
|
expect(() => validateDescriptionLength(null)).not.toThrow()
|
||||||
|
expect(() => validateDescriptionLength(undefined)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects description exceeding 400 characters', () => {
|
||||||
|
const invalidDescription = 'x'.repeat(401)
|
||||||
|
expect(() => validateDescriptionLength(invalidDescription)).toThrow(
|
||||||
|
'Description cannot exceed 400 characters.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Backend Validation Consistency', () => {
|
||||||
|
test('App and Dataset have consistent validation limits', () => {
|
||||||
|
const maxLength = 400
|
||||||
|
const validDescription = 'x'.repeat(maxLength)
|
||||||
|
const invalidDescription = 'x'.repeat(maxLength + 1)
|
||||||
|
|
||||||
|
// Both should accept exactly 400 characters
|
||||||
|
expect(validDescription.length).toBe(400)
|
||||||
|
expect(() => validateDescriptionLength(validDescription)).not.toThrow()
|
||||||
|
|
||||||
|
// Both should reject 401 characters
|
||||||
|
expect(invalidDescription.length).toBe(401)
|
||||||
|
expect(() => validateDescriptionLength(invalidDescription)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('validation error messages are consistent', () => {
|
||||||
|
const expectedErrorMessage = 'Description cannot exceed 400 characters.'
|
||||||
|
|
||||||
|
// This would be the error message from both App and Dataset backend validation
|
||||||
|
expect(expectedErrorMessage).toBe('Description cannot exceed 400 characters.')
|
||||||
|
|
||||||
|
const invalidDescription = 'x'.repeat(401)
|
||||||
|
try {
|
||||||
|
validateDescriptionLength(invalidDescription)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
expect((error as Error).message).toBe(expectedErrorMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Character Length Edge Cases', () => {
|
||||||
|
const testCases = [
|
||||||
|
{ length: 0, shouldPass: true, description: 'empty description' },
|
||||||
|
{ length: 1, shouldPass: true, description: '1 character' },
|
||||||
|
{ length: 399, shouldPass: true, description: '399 characters' },
|
||||||
|
{ length: 400, shouldPass: true, description: '400 characters (boundary)' },
|
||||||
|
{ length: 401, shouldPass: false, description: '401 characters (over limit)' },
|
||||||
|
{ length: 500, shouldPass: false, description: '500 characters' },
|
||||||
|
{ length: 1000, shouldPass: false, description: '1000 characters' },
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(({ length, shouldPass, description }) => {
|
||||||
|
test(`handles ${description} correctly`, () => {
|
||||||
|
const testDescription = length > 0 ? 'x'.repeat(length) : ''
|
||||||
|
expect(testDescription.length).toBe(length)
|
||||||
|
|
||||||
|
if (shouldPass) {
|
||||||
|
expect(() => validateDescriptionLength(testDescription)).not.toThrow()
|
||||||
|
expect(validateDescriptionLength(testDescription)).toBe(testDescription)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(() => validateDescriptionLength(testDescription)).toThrow(
|
||||||
|
'Description cannot exceed 400 characters.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@@ -82,8 +82,11 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps)
|
|||||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||||
}
|
}
|
||||||
catch {
|
catch (e: any) {
|
||||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: e.message || t('app.newApp.appCreateFailed'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
isCreatingRef.current = false
|
isCreatingRef.current = false
|
||||||
}, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor])
|
}, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor])
|
||||||
|
@@ -117,8 +117,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
if (onRefresh)
|
if (onRefresh)
|
||||||
onRefresh()
|
onRefresh()
|
||||||
}
|
}
|
||||||
catch {
|
catch (e: any) {
|
||||||
notify({ type: 'error', message: t('app.editFailed') })
|
notify({
|
||||||
|
type: 'error',
|
||||||
|
message: e.message || t('app.editFailed'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [app.id, notify, onRefresh, t])
|
}, [app.id, notify, onRefresh, t])
|
||||||
|
|
||||||
@@ -364,7 +367,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
|
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
|
||||||
<div
|
<div
|
||||||
className={cn(tags.length ? 'line-clamp-2' : 'line-clamp-4', 'group-hover:line-clamp-2')}
|
className='line-clamp-2'
|
||||||
title={app.description}
|
title={app.description}
|
||||||
>
|
>
|
||||||
{app.description}
|
{app.description}
|
||||||
|
Reference in New Issue
Block a user