Files
enterprise-addons/ai/tests/test_schema_validation.py
T

365 lines
13 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from odoo.tests import TransactionCase
from odoo.addons.ai.utils.tools_schema import validators
DUMMY_ATTRIBUTES = {'type': 'string', 'description': '...'}
DUMMY_ARRAY_ATTRIBUTES = {
'type': 'array',
'items': {
'anyOf': [{'type': 'string'}, {'type': 'number'}]}}
DUMMY_OBJECT_ATTRIBUTES = {
'type': 'object',
'properties': {
'email': {
'type': 'string',
'description': 'The email of the user',
}
},
'required': []
}
class TestParamSchemaValidation(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.wrong_array_item_type_error_message = (
"The types of the items of the 'array' param 'array_param' are not defined correctly. "
"The types of the items must be defined as ('items': {'type': 'string'}) if all items are of the same type "
"or ('items': { 'anyOf': [ {'type': 'string'}, {'type': 'number'} ] }) if items are of different types"
)
def create_param(self, param_name='param', param_attributes=None, excluded_attributes=None):
"""
The required attributes of a parameter are filled automatically with dummy values from DUMMY_ATTRIBUTES
unless they are excluded in excluded_attributes.
"""
if param_attributes is None:
param_attributes = {}
if excluded_attributes is None:
excluded_attributes = []
param_attributes = {**DUMMY_ATTRIBUTES, **param_attributes}
for excluded_attribute in excluded_attributes:
param_attributes.pop(excluded_attribute)
schema = {}
for attribute_name, attribute_value in param_attributes.items():
schema[attribute_name] = attribute_value
return {param_name: schema}
def create_array_param(self, param_name='array_param', param_attributes=None, excluded_attributes=None):
if param_attributes is None:
param_attributes = {}
if excluded_attributes is None:
excluded_attributes = []
param_attributes = {**DUMMY_ARRAY_ATTRIBUTES, **param_attributes}
schema = self.create_param(
param_name=param_name,
param_attributes=param_attributes,
excluded_attributes=excluded_attributes,
)
return schema
def create_object_param(self, param_name='object_param', param_attributes=None, excluded_attributes=None):
if param_attributes is None:
param_attributes = {}
if excluded_attributes is None:
excluded_attributes = []
param_attributes = {**DUMMY_OBJECT_ATTRIBUTES, **param_attributes}
schema = self.create_param(
param_name=param_name,
param_attributes=param_attributes,
excluded_attributes=excluded_attributes,
)
return schema
def validate_schema(self, schema, error_type=ValueError, error_message=''):
full_schema = {
'type': 'object',
'properties': schema,
'required': list(schema.keys()),
}
if error_message:
with self.assertRaisesRegex(error_type, re.escape(error_message)):
validators.validate_schema(full_schema)
else:
validators.validate_schema(full_schema)
def test_valid_schema(self):
schema = {
'user_id': {
'type': 'string',
'description': 'The unique identifier of the user whose profile is being updated. This is a permanent and immutable identifier.',
'pattern': '^[a-zA-Z0-9-]+$',
},
'preferences': {
'type': 'array',
'items': {'type': 'string'},
'description': 'A list of the user\'s preferred categories or interests. These could be used for content filtering or recommendations.',
},
'contact_info': {
'type': 'object',
'description': 'An object containing the user\'s contact details.',
'properties': {
'email': {
'type': 'string',
'description': 'The primary email address for the user. Must be a valid email format.',
'pattern': '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
},
'phone': {
'type': 'string',
'description': 'The user\'s primary phone number. Should follow a standard international format (e.g., +[country code][area code][local number]).',
'pattern': r'^\+\d{1,3}\s?\d+$',
},
},
'required': ['email'],
},
}
self.validate_schema(schema=schema)
def test_schema_with_no_params(self):
schema = {}
self.validate_schema(
schema=schema,
)
def test_unsupported_type(self):
schema = self.create_param(
param_attributes={'type': 'method'}
)
self.validate_schema(
schema=schema,
error_message="The type 'method' of parameter 'param' is wrong.",
)
def test_missing_required_attribute(self):
# Description attribute is missing
schema = self.create_param(
excluded_attributes=['type']
)
self.validate_schema(
schema=schema,
error_message="The attributes '['type']' must be defined for the parameter 'param'.",
)
def test_unsupported_attribute(self):
param_name = 'param_with_unsupported_attributes'
schema = self.create_param(
param_name=param_name,
param_attributes={'state': 'active'}
)
self.validate_schema(
schema=schema,
error_message=f"The attributes '['state']' defined for the param '{param_name}' are not supported."
)
def test_pattern_with_non_string_parameter(self):
schema = self.create_param(
param_attributes={'type': 'number', 'pattern': r'\d*'}
)
self.validate_schema(
schema=schema,
error_message=(
"The pattern attribute cannot be defined for the parameter 'param'. "
"The pattern attribute can only be defined for string parameters or arrays whose items are only of the type string."
)
)
def test_array_parameter_with_different_item_types(self):
schema = self.create_array_param()
self.validate_schema(schema=schema)
def test_array_parameter_without_item_types(self):
# items attribute is missing.
schema = self.create_array_param(
excluded_attributes={'items'}
)
self.validate_schema(
schema=schema,
error_message="The attributes '['items']' must be defined for a parameter of type 'array'. They are missing for the parameter 'array_param'.",
)
def test_array_parameter_incorrect_item_types_definition_1(self):
# items attribute must be a dict
schema = self.create_array_param(
param_attributes={'items': ['number', 'string']}
)
self.validate_schema(
schema=schema,
error_message=self.wrong_array_item_type_error_message
)
def test_array_parameter_incorrect_item_types_definition_2(self):
# The dict can only have one key, either type or anyOf
schema = self.create_array_param(
param_attributes={
'items': {'anyOf': [{'type': 'string'}], 'type': 'string'}
}
)
self.validate_schema(
schema=schema,
error_message=self.wrong_array_item_type_error_message
)
def test_array_parameter_incorrect_item_types_definition_3(self):
# The value of anyOf must be a list
schema = self.create_array_param(
param_attributes={
'items': {'anyOf': {'type': 'string'}}
}
)
self.validate_schema(
schema=schema,
error_message=self.wrong_array_item_type_error_message
)
def test_array_parameter_incorrect_item_types_definition_4(self):
# No keys other than type or anyOf are allowed
schema = self.create_array_param(
param_attributes={
'items': {'item': 'string'}
}
)
self.validate_schema(
schema=schema,
error_message=self.wrong_array_item_type_error_message
)
def test_array_parameter_incorrect_item_types_definition_5(self):
# The values inside the list of anyOf must be dicts
schema = self.create_array_param(
param_attributes={
'items': {'anyOf': ['string']}
}
)
self.validate_schema(
schema=schema,
error_message=self.wrong_array_item_type_error_message
)
def test_array_parameter_incorrect_item_types_definition_6(self):
# The types of array items can be one of ['string', 'number', 'boolean']
schema = self.create_array_param(
param_attributes={
'items': {'anyOf': [{'type': 'string'}, {'type': 'array'}]}
}
)
self.validate_schema(
schema=schema,
error_message="The type '{'type': 'array'}' of the items of the 'array' parameter 'array_param' is wrong."
)
def test_2d_array_parameter(self):
schema = self.create_array_param(
param_attributes={
'items': {
'anyOf': [{'type': 'array'}, {'type': 'string'}]
}
}
)
self.validate_schema(
schema=schema,
error_message="The type '{'type': 'array'}' of the items of the 'array' parameter 'array_param' is wrong.",
)
def test_pattern_with_string_array_items(self):
schema = self.create_array_param(
param_attributes={
'type': 'array',
'items': {'type': 'string'},
'pattern': r'\d*'
}
)
self.validate_schema(schema=schema)
def test_pattern_with_non_string_array_items(self):
schema = self.create_array_param(
param_attributes={
'type': 'array',
'items': {'anyOf': [{'type': 'string'}, {'type': 'number'}]},
'pattern': r'\d*'
}
)
self.validate_schema(
schema=schema,
error_message=(
"The pattern attribute cannot be defined for the parameter 'array_param'. "
"The pattern attribute can only be defined for string parameters or arrays whose items are only of the type string."
)
)
def test_object_parameter_with_array_property(self):
property_definition = self.create_array_param()
schema = self.create_object_param(
param_attributes={
'properties': property_definition,
}
)
self.validate_schema(schema=schema)
def test_object_parameter_without_properties_attribute(self):
schema = self.create_object_param(
excluded_attributes=['properties']
)
self.validate_schema(
schema=schema,
error_message="The attributes '['properties']' must be defined for a parameter of type 'object'. They are missing for the parameter 'object_param'.",
)
def test_object_parameter_without_required_attribute(self):
schema = self.create_object_param(
excluded_attributes=['required']
)
self.validate_schema(
schema=schema,
error_message="The attributes '['required']' must be defined for a parameter of type 'object'. They are missing for the parameter 'object_param'.",
)
def test_object_required_attribute_incorrect_definition(self):
schema = self.create_object_param(
param_attributes={'required': False}
)
self.validate_schema(
schema=schema,
error_type=TypeError,
error_message=(
"The attribute 'required' for the object param 'object_param' must be a 'list' containing "
"the required properties of the object or empty if all of them are not required."
)
)
def test_object_parameter_with_pattern_attribute(self):
schema = self.create_object_param(
param_attributes={'pattern': '^[a-zA-Z0-9]+$'}
)
self.validate_schema(
schema=schema,
error_message=(
"The pattern attribute cannot be defined for the parameter 'object_param'."
" The pattern attribute can only be defined for string parameters or arrays whose items are only of the type string."
),
)
def test_nested_object_parameter(self):
inner_object_definition = self.create_object_param(
param_name='inner_object'
)
outer_object_definition = self.create_object_param(
param_attributes={
'properties': inner_object_definition,
}
)
self.validate_schema(
schema=outer_object_definition,
error_message="Nested objects are not supported. The parameter 'inner_object' is defined as an object inside another object.",
)