Skip to content
Snippets Groups Projects
Commit 3f03cd65 authored by ml bonhomme's avatar ml bonhomme :bee: Committed by Erwan Rouchet
Browse files

Validate user_configuration in WorkerVersion's configuration

parent 2a8a38a8
No related branches found
No related tags found
1 merge request!1595Validate user_configuration in WorkerVersion's configuration
import urllib
from collections import defaultdict
from enum import Enum
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
......@@ -42,6 +44,61 @@ class WorkerSerializer(WorkerLightSerializer):
fields = WorkerLightSerializer.Meta.fields + ('repository_id', )
class UserConfigurationFieldType(Enum):
Int = 'int'
Float = 'float'
String = 'str'
Enum = 'enum'
class UserConfigurationFieldSerializer(serializers.Serializer):
title = serializers.CharField()
type = EnumField(UserConfigurationFieldType)
required = serializers.BooleanField(default=False)
choices = serializers.ListField(required=False, allow_empty=False, allow_null=True)
def to_internal_value(self, data):
errors = defaultdict(list)
allowed_fields = ['title', 'type', 'required', 'default', 'choices']
data_types = {
UserConfigurationFieldType.Int: serializers.IntegerField,
UserConfigurationFieldType.Float: serializers.FloatField,
UserConfigurationFieldType.String: serializers.CharField,
UserConfigurationFieldType.Enum: serializers.ChoiceField
}
for field in data:
if field not in allowed_fields:
errors[field].append(
'Configurable properties can only be defined using the following keys: title, type, required, default, choices.'
)
default_value = data.get('default')
data = super().to_internal_value(data)
field_type = data.get('type')
choices = data.get('choices')
if choices:
if field_type != UserConfigurationFieldType.Enum:
errors['choices'].append('The "choices" field can only be set for an "enum" type property.')
# If the configuration parameter is of enum type, an eventual default value won't match the field type
if default_value and default_value not in choices:
errors['default'].append(f'{default_value} is not an available choice.')
elif default_value:
try:
data_type = data_types[field_type]
data_type().to_internal_value(default_value)
except ValidationError:
errors['default'].append(f'Default value is not of type {field_type.value}.')
except KeyError:
errors['default'].append(f'Cannot check type: {field_type.value}.')
if errors:
raise ValidationError(errors)
return data
class WorkerVersionSerializer(serializers.ModelSerializer):
"""
Serialize a worker version
......@@ -86,6 +143,20 @@ class WorkerVersionSerializer(serializers.ModelSerializer):
except Revision.DoesNotExist:
raise ValidationError({'revision': ['Revision with this ID does not exist.']})
def validate_configuration(self, configuration):
errors = defaultdict(list)
user_configuration = configuration.get('user_configuration')
if not user_configuration:
return configuration
field = serializers.DictField(child=UserConfigurationFieldSerializer())
try:
field.to_internal_value(user_configuration)
except ValidationError as e:
errors['user_configuration'].append(e.detail)
if errors:
raise ValidationError(errors)
return configuration
def validate(self, data):
# Assert that a version set to available has a docker image
state = data.get('state') or self.instance and self.instance.state
......
......@@ -602,6 +602,272 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
self.assertEqual(data['state'], 'created')
self.assertEqual(data['gpu_usage'], 'disabled')
def test_no_user_configuration_ok(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {"beep": "boop"},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_valid_user_configuration(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"demo_integer": {"title": "Demo Integer", "type": "int", "required": True, "default": 1}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()['configuration'], {
"user_configuration": {
"demo_integer": {
"title": "Demo Integer",
"type": "int",
"required": True,
"default": 1
}
}
})
def test_valid_user_configuration_enum(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"demo_choice": {"title": "Decisions", "type": "enum", "required": True, "default": 1, "choices": [1, 2, 3]}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()['configuration'], {
"user_configuration": {
"demo_choice": {
"title": "Decisions",
"type": "enum",
"required": True,
"default": 1,
"choices": [1, 2, 3]
}
}
})
def test_valid_user_configuration_not_list_choices(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"demo_choice": {"title": "Decisions", "type": "enum", "required": True, "default": 1, "choices": "eeee"}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"demo_choice": {
"choices": ["Expected a list of items but got type \"str\"."]
}
}]
}
})
def test_invalid_user_configuration_not_dict(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": "non"
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {"configuration": {"user_configuration": [["Expected a dictionary of items but got type \"str\"."]]}})
def test_invalid_user_configuration_wrong_field_type(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"something": {
"title": "some thing",
"type": "uh oh",
"required": 2
}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"something": {
"required": ["Must be a valid boolean."],
"type": ["Value is not of type UserConfigurationFieldType"]
}
}]
}
})
def test_invalid_user_configuration_wrong_default_type(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"one_float": {
"title": "a float",
"type": "float",
"default": "bonjour",
"required": True
}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"one_float": {
"default": ["Default value is not of type float."]
}
}]
}
})
def test_invalid_user_configuration_choices_no_enum(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"something": {
"title": "some thing",
"type": "int",
"required": False,
"choices": [1, 2, 3]
}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [{
"something": {
"choices": ["The \"choices\" field can only be set for an \"enum\" type property."]
}
}]
}
})
def test_invalid_user_configuration_missing_key(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"demo_integer": {"type": "int", "required": True, "default": 1}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json(),
{
"configuration": {
"user_configuration": [{
"demo_integer": {
"title": ["This field is required."]
}
}]
}
}
)
def test_invalid_user_configuration_invalid_key(self):
self.client.force_login(self.internal_user)
response = self.client.post(
reverse("api:worker-versions", kwargs={"pk": str(self.worker_2.id)}),
data={
"revision": str(self.rev2.id),
"configuration": {
"user_configuration": {
"demo_integer": {
"title": "an integer",
"type": "int",
"required": True,
"default": 1,
"some_key": "oh no",
}
}
},
"gpu_usage": "disabled",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json(), {
"configuration": {
"user_configuration": [
{
"demo_integer": {
"some_key": ["Configurable properties can only be defined using the following keys: title, type, required, default, choices."]
}
}
]
}
})
def test_retrieve_version_invalid_id(self):
self.client.force_login(self.user)
response = self.client.get(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment