From 5a9e3cda500fa0e89f4132eb4a96062ef793a501 Mon Sep 17 00:00:00 2001 From: Nicolas Marcq Date: Mon, 8 Jan 2024 15:53:40 +0100 Subject: [PATCH] [Feature] Survey validator --- CHANGELOG.md | 4 + Squest/settings.py | 1 + Squest/utils/plugin_controller.py | 67 +++++++- Squest/utils/squest_form.py | 5 +- docker-compose.yml | 2 +- docs/configuration/squest_settings.md | 10 ++ .../{validators.md => field_validators.md} | 4 + docs/manual/advanced/survey_validators.md | 89 ++++++++++ docs/manual/service_catalog/survey.md | 4 +- mkdocs.yml | 3 +- plugins/survey_validators/__init__.py | 0 .../api/serializers/request_serializers.py | 76 +++++++-- .../api/views/request_api_views.py | 16 +- service_catalog/forms/__init__.py | 1 + service_catalog/forms/form_utils.py | 39 ++++- service_catalog/forms/operation_forms.py | 29 +++- .../forms/operation_request_forms.py | 17 +- .../forms/service_request_forms.py | 17 +- .../forms/tower_survey_field_form.py | 2 +- .../migrations/0041_operation_validators.py | 18 ++ service_catalog/models/__init__.py | 2 +- service_catalog/models/instance.py | 11 ++ service_catalog/models/operations.py | 13 ++ service_catalog/views/catalog_views.py | 2 + tests/setup/setup_awx.py | 2 +- tests/setup/setup_instance.py | 17 +- tests/setup/setup_operation.py | 9 +- .../survey_validators_test/__init__.py | 0 .../survey_validators_test/survey_test.py | 45 +++++ tests/test_plugins/test_plugin_controller.py | 34 +++- .../test_operation_request_create.py | 6 +- .../test_serializers/test_survey_validator.py | 146 ++++++++++++++++ .../test_views/test_survey_validator.py | 83 +++++++++ .../test_forms/test_survey_validator.py | 126 ++++++++++++++ .../test_models/test_survey_validator.py | 159 ++++++++++++++++++ .../test_serializers/__init__.py | 0 .../test_common/test_survey_validator.py | 79 +++++++++ 37 files changed, 1078 insertions(+), 60 deletions(-) rename docs/manual/advanced/{validators.md => field_validators.md} (95%) create mode 100644 docs/manual/advanced/survey_validators.md create mode 100644 plugins/survey_validators/__init__.py create mode 100644 service_catalog/migrations/0041_operation_validators.py create mode 100644 tests/test_plugins/survey_validators_test/__init__.py create mode 100644 tests/test_plugins/survey_validators_test/survey_test.py create mode 100644 tests/test_service_catalog/test_api/test_serializers/test_survey_validator.py create mode 100644 tests/test_service_catalog/test_api/test_views/test_survey_validator.py create mode 100644 tests/test_service_catalog/test_forms/test_survey_validator.py create mode 100644 tests/test_service_catalog/test_models/test_survey_validator.py create mode 100644 tests/test_service_catalog/test_serializers/__init__.py create mode 100644 tests/test_service_catalog/test_views/test_common/test_survey_validator.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 001119b6b..ba996fccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Hide disabled operations from Instance view +## Feature + +- Add SurveyValidator on Operation. It allows you to implement your own validation logic with Python on the entire form. See documentation [here](https://hewlettpackard.github.io/squest/latest/manual/advanced/survey_validators/) + # 2.4.1 2023-12-18 ## Fix diff --git a/Squest/settings.py b/Squest/settings.py index a3e5a9c91..bbd3956c0 100644 --- a/Squest/settings.py +++ b/Squest/settings.py @@ -523,6 +523,7 @@ def backup_filename(databasename, servername, datetime, extension, content_type) # Plugins # ----------------------------------------- FIELD_VALIDATOR_PATH = "plugins/field_validators" +SURVEY_VALIDATOR_PATH = "plugins/survey_validators" # ----------------------------------------- # SQL Debug diff --git a/Squest/utils/plugin_controller.py b/Squest/utils/plugin_controller.py index 628f4dbc1..c7925f7a3 100644 --- a/Squest/utils/plugin_controller.py +++ b/Squest/utils/plugin_controller.py @@ -1,5 +1,9 @@ import logging +from importlib.machinery import SourceFileLoader +import inspect import os +from pydoc import locate +import re from django.conf import settings @@ -9,10 +13,16 @@ logger = logging.getLogger(__name__) +def full_path_to_dotted_path(path): + # /foo/bar/myfile.py -> foo.bar.myfile + path = re.sub(r"\.py", "", path) + path = re.sub(r"/", ".", path) + return path + class PluginController: @classmethod - def get_user_provisioned_validators(cls): + def get_user_provisioned_field_validators(cls): filepath = settings.FIELD_VALIDATOR_PATH file_list = os.listdir(filepath) for forbidden_word in ["__init__.py", "__pycache__"]: @@ -24,24 +34,69 @@ def get_user_provisioned_validators(cls): returned_list.sort() return returned_list + @classmethod + def get_user_provisioned_survey_validators(cls): + def is_validator(obj): + """ + Returns True if the object is a Script. + """ + from service_catalog.forms import SurveyValidator + try: + return issubclass(obj, SurveyValidator) and obj != SurveyValidator + except TypeError: + return False + + def python_name(full_path): + # /foo/bar/myfile.py -> myfile + path, filename = os.path.split(full_path) + name = os.path.splitext(filename)[0] + if name == "__init__": + # File is a package + return os.path.basename(path) + else: + return name + + scripts = list() + for filename in os.listdir(settings.SURVEY_VALIDATOR_PATH): + if filename in ["__init__.py", "__pycache__"]: + continue + full_path = os.path.join(settings.SURVEY_VALIDATOR_PATH, filename) + loader = SourceFileLoader(python_name(filename), full_path) + module = loader.load_module() + for name, klass in inspect.getmembers(module, is_validator): + dotted_path = f"{python_name(filename)}.{klass.__name__}" + scripts.append(dotted_path) + return scripts + @classmethod def get_ui_field_validator_def(cls, validator_file): - return cls._load_validator_module(module_name=validator_file, definition_kind=VALIDATE_UI_DEFINITION_NAME) + return cls._load_field_validator_module(module_name=validator_file, definition_kind=VALIDATE_UI_DEFINITION_NAME) @classmethod def get_api_field_validator_def(cls, validator_file): - return cls._load_validator_module(module_name=validator_file, definition_kind=VALIDATE_API_DEFINITION_NAME) + return cls._load_field_validator_module(module_name=validator_file, + definition_kind=VALIDATE_API_DEFINITION_NAME) @classmethod - def _load_validator_module(cls, module_name, definition_kind): + def get_survey_validator_def(cls, validator_path): + return locate(f"{full_path_to_dotted_path(settings.SURVEY_VALIDATOR_PATH)}.{validator_path}") + + @classmethod + def _load_field_validator_module(cls, module_name, definition_kind): + logger.warning("Deprecation warning: Please switch to SurveyValidator") + filepath = settings.FIELD_VALIDATOR_PATH + return cls._load_validator_module(module_name, definition_kind, filepath) + + @staticmethod + def _load_validator_module(module_name, definition_kind, filepath): """ Dynamically load a validator definition from a python file - :param module_name: name of the python file that contains field validator definitions + :param module_name: name of the python file that contains validator definitions :param definition_kind: UI or API :return: """ try: - mod = __import__(f"{settings.FIELD_VALIDATOR_PATH.replace('/','.')}.{module_name}", + mod = __import__(f"{filepath.replace('/', '.')}.{module_name}", fromlist=[definition_kind]) klass = getattr(mod, definition_kind) return klass diff --git a/Squest/utils/squest_form.py b/Squest/utils/squest_form.py index e053d498b..a36d3786a 100644 --- a/Squest/utils/squest_form.py +++ b/Squest/utils/squest_form.py @@ -16,8 +16,9 @@ def __init__(self, *args, **kwargs): def is_valid(self): returned_value = super(SquestForm, self).is_valid() for field_name in self.errors.keys(): - current_class = self.fields.get(field_name).widget.attrs.get('class', '') - self.fields.get(field_name).widget.attrs['class'] = f"{current_class} {self.error_css_class}" + if field_name != "__all__": + current_class = self.fields.get(field_name).widget.attrs.get('class', '') + self.fields.get(field_name).widget.attrs['class'] = f"{current_class} {self.error_css_class}" return returned_value def beautify(self): diff --git a/docker-compose.yml b/docker-compose.yml index 92df20984..adfe171fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: - django_static:/app/static - django_media:/app/media - backup:/app/backup - - ./plugins/field_validators:/app/plugins/field_validators + - ./plugins:/app/plugins depends_on: - db - rabbitmq diff --git a/docs/configuration/squest_settings.md b/docs/configuration/squest_settings.md index ed0d9c625..6364f9814 100644 --- a/docs/configuration/squest_settings.md +++ b/docs/configuration/squest_settings.md @@ -337,10 +337,20 @@ Change the format of all date in Squest UI. Based on Python [strftime](https://s ### FIELD_VALIDATOR_PATH +!!!warning + + FIELD_VALIDATOR_PATH is now deprecated. Please use [SURVEY_VALIDATOR_PATH](#survey_validator_path) instead. + **Default:** `plugins/field_validators` Path to form field validation modules. +### SURVEY_VALIDATOR_PATH + +**Default:** `plugins/survey_validators` + +Path to SurveyValidator modules. + ## Redis ### REDIS_CACHE_USER diff --git a/docs/manual/advanced/validators.md b/docs/manual/advanced/field_validators.md similarity index 95% rename from docs/manual/advanced/validators.md rename to docs/manual/advanced/field_validators.md index dbe16ec52..7f005adfe 100644 --- a/docs/manual/advanced/validators.md +++ b/docs/manual/advanced/field_validators.md @@ -1,5 +1,9 @@ # Field validators +!!!warning + + Field validators feature is now deprecated. Please use [survey validator](survey_validators.md) instead. + Field validators are Python modules that can be added as plugin to perform custom checks on an [operation survey field](../service_catalog/operation.md#survey). ## Create a field validator diff --git a/docs/manual/advanced/survey_validators.md b/docs/manual/advanced/survey_validators.md new file mode 100644 index 000000000..2117bdf02 --- /dev/null +++ b/docs/manual/advanced/survey_validators.md @@ -0,0 +1,89 @@ +# Survey validators + +Survey validators are Python modules that can be added as plugin. It allows users to implement their own validation +logic on a day1 or day2 operation against the full survey. + +## Creating survey validator + +Create a Python file in **SURVEY_VALIDATOR_PATH** (default is `plugins/survey_validators`). +Create Python class that inherit from SurveyValidator with a method `validate_survey`. + +```python +# plugins/survey_validators/MySurveyValidator.py +from service_catalog.forms import SurveyValidator + +class MyCustomValidatorFoo(SurveyValidator): + def validate_survey(self): + # Implement your own logic here + pass +``` + +## SurveyValidator attributes + +### survey + +This is a dict containing survey + request_comment. Keys are variable name. +type: dict + +```bash +>>> print(self.survey) +{ + "request_comment": "commentary sent by user" + "ram_gb": 8, + "vcpu": 4 +} +``` + +### user + +User requesting operation. +type: django.contrib.auth.models.User + +### operation + +Operation requested. +type: service_catalog.models.Operation + +### instance + +Instance targeted. +type: service_catalog.models.Instance + +!!!note + For day 1 operation `self.instance` is a FakeInstance object that contains only **name** and **quota_scope** without `save` method. + The real Instance object is created after validation. + +## SurveyValidator method + +### validate_survey(self) + +Redefine it to implement your own logic. + +### fail(self, message, field="\_\_all\_\_") + +Raise an exception and display message on UI/API. + +## Set validator to a form field + +In Squest, edit an Operation to set validators. Multiples validators can be added, validators are executed in alphabetical order by script name and class name. + +## Example + +This validator will always fail if: + +- ram and cpu are both equal 1 +- It's not the weekend yet + +```python +from service_catalog.forms import SurveyValidator +import datetime + +class ValidatorForVM(SurveyValidator): + def validate_survey(self): + if self.survey.get("ram") == 1 and self.survey.get("vcpu") == 1: + self.fail("Forbidden: you cannot use ram=1 and cpu=1") + + weekday = datetime.datetime.today().weekday() + if weekday < 5: + self.fail("Sorry it's not the weekend yet") +``` diff --git a/docs/manual/service_catalog/survey.md b/docs/manual/service_catalog/survey.md index d3d2f8a42..79972ddf7 100644 --- a/docs/manual/service_catalog/survey.md +++ b/docs/manual/service_catalog/survey.md @@ -49,8 +49,8 @@ Full `instance` and `user `object definition can be retrieved through the [API d ## Validators -Field validators are python modules that can be added as plugin to perform a custom check on a form field. -See related [documentation here](../advanced/validators.md). +SurveyValidator are python modules that can be added as plugins to perform a custom check on your form. +See related [documentation here](../advanced/survey_validators.md). ## Attribute definition diff --git a/mkdocs.yml b/mkdocs.yml index 632e4e16a..0d8229560 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,7 +54,8 @@ nav: - Advanced: - manual/advanced/filters.md - manual/advanced/jinja.md - - manual/advanced/validators.md + - manual/advanced/field_validators.md + - manual/advanced/survey_validators.md - manual/advanced/ldap.md - Notifications: manual/notifications.md - Administration: diff --git a/plugins/survey_validators/__init__.py b/plugins/survey_validators/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/service_catalog/api/serializers/request_serializers.py b/service_catalog/api/serializers/request_serializers.py index 092157d54..ac9ea7f85 100644 --- a/service_catalog/api/serializers/request_serializers.py +++ b/service_catalog/api/serializers/request_serializers.py @@ -1,6 +1,6 @@ from json import dumps, loads -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import ValidationError, PermissionDenied from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.serializers import ModelSerializer, CharField @@ -8,7 +8,10 @@ from profiles.api.serializers.user_serializers import UserSerializerNested from profiles.models import Scope from service_catalog.api.serializers import DynamicSurveySerializer, InstanceReadSerializer -from service_catalog.models.instance import Instance +from service_catalog.models import InstanceState, OperationType + + +from service_catalog.models import Instance, FakeInstance from service_catalog.models.message import RequestMessage from service_catalog.models.request import Request @@ -41,21 +44,32 @@ def __init__(self, *args, **kwargs): def validate(self, data): super(ServiceRequestSerializer, self).validate(data) quota_scope = data.get("quota_scope") - fill_in_survey = data.get("fill_in_survey") + fill_in_survey = data.get("fill_in_survey", {}) + request_comment = data.get("request_comment") # validate the quota if set on one of the fill_in_survey - if fill_in_survey is not None: - for field_name, value in fill_in_survey.items(): - # get the tower field - tower_field = self.operation.tower_survey_fields.get(variable=field_name) - if tower_field.attribute_definition is not None: - # try to find the field in the quota linked to the scope - quota_set_on_attribute = quota_scope.quotas.filter(attribute_definition=tower_field.attribute_definition) - if quota_set_on_attribute.exists(): - quota_set_on_attribute = quota_set_on_attribute.first() - if value > quota_set_on_attribute.available: - raise ValidationError({"fill_in_survey": - f"Quota limit reached on '{field_name}'. " - f"Available: {quota_set_on_attribute.available}"}) + for field_name, value in fill_in_survey.items(): + # get the tower field + tower_field = self.operation.tower_survey_fields.get(variable=field_name) + if tower_field.attribute_definition is not None: + # try to find the field in the quota linked to the scope + quota_set_on_attribute = quota_scope.quotas.filter( + attribute_definition=tower_field.attribute_definition) + if quota_set_on_attribute.exists(): + quota_set_on_attribute = quota_set_on_attribute.first() + if value > quota_set_on_attribute.available: + raise ValidationError({"fill_in_survey": + f"Quota limit reached on '{field_name}'. " + f"Available: {quota_set_on_attribute.available}"}) + fill_in_survey.update({"request_comment": request_comment}) + for validators in self.operation.get_validators(): + # load dynamically the user provided validator + validators( + survey=fill_in_survey, + user=self.user, + operation=self.operation, + instance=FakeInstance(quota_scope=quota_scope, name=data.get("squest_instance_name")), + form=None + )._validate() return data def save(self): @@ -117,6 +131,36 @@ def save(self, **kwargs): send_mail_request_update(target_request=new_request, user_applied_state=new_request.user, message=message) return new_request + def validate(self, data): + super(OperationRequestSerializer, self).validate(data) + + if self.operation.is_admin_operation and not self.user.has_perm("service_catalog.admin_request_on_instance"): + raise PermissionDenied + if not self.operation.is_admin_operation and not self.user.has_perm("service_catalog.request_on_instance"): + raise PermissionDenied + if self.squest_instance.state not in [InstanceState.AVAILABLE]: + raise PermissionDenied("Instance not available") + if self.operation.enabled is False: + raise PermissionDenied(f"Operation is not enabled.") + if self.operation.service.id != self.squest_instance.service.id: + raise PermissionDenied("Operation service and instance service doesn't match") + if self.operation.type not in [OperationType.UPDATE, OperationType.DELETE]: + raise PermissionDenied("Operation type UPDATE and DELETE only") + fill_in_survey = data.get("fill_in_survey") + request_comment = data.get("request_comment") + fill_in_survey.update({"request_comment": request_comment}) + + for validators in self.operation.get_validators(): + # load dynamically the user provided validator + validators( + survey=fill_in_survey, + user=self.user, + operation=self.operation, + instance=self.squest_instance, + form=None + )._validate() + return data + class RequestSerializer(ModelSerializer): class Meta: diff --git a/service_catalog/api/views/request_api_views.py b/service_catalog/api/views/request_api_views.py index c9a0993a9..5245b32a1 100644 --- a/service_catalog/api/views/request_api_views.py +++ b/service_catalog/api/views/request_api_views.py @@ -18,11 +18,12 @@ class RequestList(SquestListAPIView): def get_queryset(self): return Request.get_queryset_for_user(self.request.user, 'service_catalog.view_request').prefetch_related( - "user", "operation", "instance__requester","instance__requester__profile","instance__resources","instance__requester__groups" ,"instance__quota_scope", "instance__service", - "operation__service", "approval_workflow_state", "approval_workflow_state__approval_workflow", - "approval_workflow_state__current_step", - "approval_workflow_state__current_step__approval_step", "approval_workflow_state__approval_step_states" - ) + "user", "operation", "instance__requester", "instance__requester__profile", "instance__resources", + "instance__requester__groups", "instance__quota_scope", "instance__service", + "operation__service", "approval_workflow_state", "approval_workflow_state__approval_workflow", + "approval_workflow_state__current_step", + "approval_workflow_state__current_step__approval_step", "approval_workflow_state__approval_step_states" + ) def get_serializer_class(self): if self.request.user.has_perm("service_catalog.view_admin_survey"): @@ -63,11 +64,6 @@ def create(self, request, *args, **kwargs): Operation, id=kwargs.get('operation_id'), type__in=[OperationType.UPDATE, OperationType.DELETE], enabled=True) - if operation.is_admin_operation and not self.request.user.has_perm("service_catalog.admin_request_on_instance"): - raise PermissionDenied - if not operation.is_admin_operation and not self.request.user.has_perm("service_catalog.request_on_instance"): - raise PermissionDenied - serializer = self.get_serializer(operation=operation, instance=self.get_object(), user=request.user, data=request.data) serializer.is_valid(raise_exception=True) diff --git a/service_catalog/forms/__init__.py b/service_catalog/forms/__init__.py index 72b90ff17..af0096aa5 100644 --- a/service_catalog/forms/__init__.py +++ b/service_catalog/forms/__init__.py @@ -11,3 +11,4 @@ from .support_message_forms import * from .support_request_forms import * from .tower_server_forms import * +from .form_utils import * diff --git a/service_catalog/forms/form_utils.py b/service_catalog/forms/form_utils.py index e06bf26e2..96a3a040a 100644 --- a/service_catalog/forms/form_utils.py +++ b/service_catalog/forms/form_utils.py @@ -1,7 +1,11 @@ import logging - from jinja2 import Template from jinja2.exceptions import UndefinedError +import inspect + +from rest_framework.exceptions import ValidationError + +from service_catalog.models import Instance logger = logging.getLogger(__name__) @@ -25,3 +29,36 @@ def template_field(cls, jinja_template_string, template_data_dict): logger.warning(f"[template_field] templating error: {e.message}") pass return templated_string + + + + +class SurveyValidator: + def __init__(self, survey, user, operation, instance, form=None): + self.survey = survey + self.user = user + self.operation = operation + self.instance = instance + self._form = form + + def validate_survey(self): + pass + + def _validate(self): + self.validate_survey() + logger.info(f"[Form utils] User validator plugin loaded: {inspect.getfile(self.__class__)}") + + def fail(self, message, field="__all__"): + """ Raise an exception on "field" with message. + Keyword arguments: + message -- str -- message you want to display + field -- str -- field that contains error, default is "__all_"" + """ + logger.info( + f"Request blocked by validator {inspect.getfile(self.__class__)} (operation:{self.operation}, user:{self.user}), field:{field}, message: {message}" + ) + + if self._form: + self._form.add_error(field, message) + else: + raise ValidationError({field: message}) diff --git a/service_catalog/forms/operation_forms.py b/service_catalog/forms/operation_forms.py index bd0f38fbe..da3c15c51 100644 --- a/service_catalog/forms/operation_forms.py +++ b/service_catalog/forms/operation_forms.py @@ -1,11 +1,38 @@ +from django.forms import MultipleChoiceField, SelectMultiple + +from Squest.utils.plugin_controller import PluginController from Squest.utils.squest_model_form import SquestModelForm from service_catalog.models import Operation class OperationForm(SquestModelForm): + validators = MultipleChoiceField(label="Validators", + required=False, + choices=[], + widget=SelectMultiple(attrs={'data-live-search': "true"})) + class Meta: model = Operation fields = ["service", "name", "description", "job_template", "type", "process_timeout_second", "auto_accept", "auto_process", "enabled", "is_admin_operation", "extra_vars", "default_inventory_id", "default_limits", "default_tags", "default_skip_tags", "default_credentials_ids", "default_verbosity", - "default_diff_mode", "default_job_type"] + "default_diff_mode", "default_job_type", "validators"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + validator_choices = list() + validator_files = [(file_name, file_name) for file_name in + PluginController.get_user_provisioned_survey_validators()] + validator_choices.extend(validator_files) + self.fields['validators'].choices = validator_choices + if self.instance is not None: + if self.instance.validators is not None: + # Converting comma separated string to python list + instance_validator_as_list = self.instance.validators.split(",") + # set the current value + self.initial["validators"] = instance_validator_as_list + + def clean_validators(self): + if not self.cleaned_data['validators']: + return None + return ",".join(self.cleaned_data['validators']) diff --git a/service_catalog/forms/operation_request_forms.py b/service_catalog/forms/operation_request_forms.py index 98bbb2a5b..a6b82363d 100644 --- a/service_catalog/forms/operation_request_forms.py +++ b/service_catalog/forms/operation_request_forms.py @@ -1,13 +1,13 @@ from django import forms +from Squest.utils.squest_form import SquestForm from service_catalog.forms.form_generator import FormGenerator from service_catalog.models import Request, RequestMessage EXCLUDED_SURVEY_FIELDS = ["request_comment"] -class OperationRequestForm(forms.Form): - +class OperationRequestForm(SquestForm): request_comment = forms.CharField(label="Comment", help_text="Add a comment to your request", widget=forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), @@ -40,3 +40,16 @@ def save(self): from service_catalog.mail_utils import send_mail_request_update send_mail_request_update(target_request=new_request, user_applied_state=new_request.user, message=message) return new_request + + def clean(self): + super().clean() + for validators in self.operation.get_validators(): + # load dynamically the user provided validator + validators( + survey=self.cleaned_data, + user=self.user, + operation=self.operation, + instance=self.instance, + form=self + )._validate() + return self.cleaned_data diff --git a/service_catalog/forms/service_request_forms.py b/service_catalog/forms/service_request_forms.py index 018b6246b..32b37508a 100644 --- a/service_catalog/forms/service_request_forms.py +++ b/service_catalog/forms/service_request_forms.py @@ -4,7 +4,7 @@ from Squest.utils.squest_model_form import SquestModelForm from profiles.models.scope import Scope from service_catalog.forms.form_generator import FormGenerator -from service_catalog.models import Instance, Request, RequestMessage +from service_catalog.models import Instance, FakeInstance, Request, RequestMessage class ServiceInstanceForm(SquestModelForm): @@ -40,6 +40,7 @@ def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) self.operation = kwargs.pop('operation', None) self.quota_scope = kwargs.pop('quota_scope', None) + self.instance_name = kwargs.pop('instance_name', None) super(ServiceRequestForm, self).__init__(*args, **kwargs) form_generator = FormGenerator(user=self.user, operation=self.operation, quota_scope=self.quota_scope) @@ -66,3 +67,17 @@ def save(self, squest_instance): from service_catalog.mail_utils import send_mail_request_update send_mail_request_update(target_request=new_request, user_applied_state=self.user, message=message) return new_request + + def clean(self): + super().clean() + for validators in self.operation.get_validators(): + # load dynamically the user provided validator + validators( + survey=self.cleaned_data, + user=self.user, + operation=self.operation, + instance=FakeInstance(name=self.instance_name,quota_scope=self.quota_scope), + form=self + )._validate() + + return self.cleaned_data diff --git a/service_catalog/forms/tower_survey_field_form.py b/service_catalog/forms/tower_survey_field_form.py index da5c4170d..8f04ea146 100644 --- a/service_catalog/forms/tower_survey_field_form.py +++ b/service_catalog/forms/tower_survey_field_form.py @@ -16,7 +16,7 @@ class TowerSurveyFieldForm(SquestModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) validator_choices = list() - validator_files = [(file_name, file_name) for file_name in PluginController.get_user_provisioned_validators()] + validator_files = [(file_name, file_name) for file_name in PluginController.get_user_provisioned_field_validators()] validator_choices.extend(validator_files) self.fields['validators'].choices = validator_choices self.fields['attribute_definition'].choices = [(None, "---------")] diff --git a/service_catalog/migrations/0041_operation_validators.py b/service_catalog/migrations/0041_operation_validators.py new file mode 100644 index 000000000..6313981f1 --- /dev/null +++ b/service_catalog/migrations/0041_operation_validators.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-12-20 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('service_catalog', '0040_alter_towersurveyfield_unique_together'), + ] + + operations = [ + migrations.AddField( + model_name='operation', + name='validators', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Survey validators'), + ), + ] diff --git a/service_catalog/models/__init__.py b/service_catalog/models/__init__.py index c9b46fab9..a9e459d7f 100644 --- a/service_catalog/models/__init__.py +++ b/service_catalog/models/__init__.py @@ -8,7 +8,7 @@ from service_catalog.models.job_templates import JobTemplate from service_catalog.models.services import Service from service_catalog.models.operations import Operation -from service_catalog.models.instance import Instance +from service_catalog.models.instance import Instance, FakeInstance from service_catalog.models.request import Request from service_catalog.models.message import Message, RequestMessage, SupportMessage from service_catalog.models.support import Support diff --git a/service_catalog/models/instance.py b/service_catalog/models/instance.py index be1dde4a8..5ddc861b4 100644 --- a/service_catalog/models/instance.py +++ b/service_catalog/models/instance.py @@ -220,3 +220,14 @@ def on_change(cls, sender, instance, *args, **kwargs): @receiver(pre_delete, sender=Instance) def pre_delete(sender, instance, **kwargs): instance.delete_linked_resources() + + +class FakeInstance(Instance): + class Meta: + proxy = True + managed = False + + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + return False diff --git a/service_catalog/models/operations.py b/service_catalog/models/operations.py index d96200743..cf14b889d 100644 --- a/service_catalog/models/operations.py +++ b/service_catalog/models/operations.py @@ -5,6 +5,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from Squest.utils.plugin_controller import PluginController from Squest.utils.squest_model import SquestModel from service_catalog.models.job_templates import JobTemplate from service_catalog.models.operation_type import OperationType @@ -49,6 +50,18 @@ class Operation(SquestModel): help_text="Jinja supported. Show changes") default_job_type = CharField(max_length=500, blank=True, null=True, help_text="Jinja supported. Job template type") + validators = CharField(null=True, blank=True, max_length=200, verbose_name="Survey validators") + + def get_validators(self): + validators = list() + if self.validators is not None: + all_validators = self.validators.split(",") + all_validators.sort() + for validator_file in all_validators: + validator = PluginController.get_survey_validator_def(validator_file) + if validator: + validators.append(validator) + return validators def __str__(self): return f"{self.name} ({self.service})" diff --git a/service_catalog/views/catalog_views.py b/service_catalog/views/catalog_views.py index 04478a5ad..571c986a1 100644 --- a/service_catalog/views/catalog_views.py +++ b/service_catalog/views/catalog_views.py @@ -37,8 +37,10 @@ def get_form_kwargs(self, step): if step == "1": # add data from step 0 scope_id = self.storage.data['step_data']['0']['0-quota_scope'][0] + instance_name = self.storage.data['step_data']['0']['0-name'][0] quota_scope = get_object_or_404(Scope, id=scope_id) kwargs.update({'quota_scope': quota_scope}) + kwargs.update({'instance_name': instance_name}) return kwargs def get_template_names(self): diff --git a/tests/setup/setup_awx.py b/tests/setup/setup_awx.py index ca6c85441..b31ced72c 100644 --- a/tests/setup/setup_awx.py +++ b/tests/setup/setup_awx.py @@ -32,7 +32,7 @@ def setUp(self): "required": False, "variable": "ram", "new_question": False, - "question_name": "ram", + "question_name": "RAM", "question_description": "" } ] diff --git a/tests/setup/setup_instance.py b/tests/setup/setup_instance.py index 65beeb486..917eb8a04 100644 --- a/tests/setup/setup_instance.py +++ b/tests/setup/setup_instance.py @@ -1,6 +1,7 @@ from django.test import TestCase from rest_framework.test import APITestCase +from profiles.models import Scope from service_catalog.models import Instance from tests.setup import SetupOperationCommon, SetupTeamCommon @@ -13,33 +14,33 @@ def setUp(self): # Org 1 self.instance_1_org1 = Instance.objects.create(name="Instance 1 - Org 1", - quota_scope=self.org1, + quota_scope=Scope.objects.get(id=self.org1.id), service=self.service_1) # Org 2 - Team 1 self.instance_2_team1org2 = Instance.objects.create(name="Instance 2 - Org 2 - Team 1", - quota_scope=self.team1org2, + quota_scope=Scope.objects.get(id=self.team1org2.id), service=self.service_1) self.instance_3_team1org2 = Instance.objects.create(name="Instance 3 - Org 2 - Team 1", - quota_scope=self.team1org2, + quota_scope=Scope.objects.get(id=self.team1org2.id), service=self.service_1) # Org 2 - Team 2 self.instance_4_team2org2 = Instance.objects.create(name="Instance 4 - Org 2 - Team 2", - quota_scope=self.team2org2, + quota_scope=Scope.objects.get(id=self.team2org2.id), service=self.service_1) self.instance_5_team2org2 = Instance.objects.create(name="Instance 5 - Org 2 - Team 2", - quota_scope=self.team2org2, + quota_scope=Scope.objects.get(id=self.team2org2.id), service=self.service_1) # Org 2 self.instance_6_org2 = Instance.objects.create(name="Instance 6 - Org 2 ", - quota_scope=self.org2, + quota_scope=Scope.objects.get(id=self.org2.id), service=self.service_1) # Org 3 - Team 1 self.instance_7_team1org3 = Instance.objects.create(name="Instance 7 - Org 3 - Team 1", - quota_scope=self.team1org3, + quota_scope=Scope.objects.get(id=self.team1org3.id), service=self.service_1) # Org 3 self.instance_8_org3 = Instance.objects.create(name="Instance 7 - Org 3", - quota_scope=self.org3, + quota_scope=Scope.objects.get(id=self.org3.id), service=self.service_1) print("SetupInstanceCommon finished") diff --git a/tests/setup/setup_operation.py b/tests/setup/setup_operation.py index e81829f2e..13904e344 100644 --- a/tests/setup/setup_operation.py +++ b/tests/setup/setup_operation.py @@ -1,7 +1,7 @@ from django.test import TestCase from rest_framework.test import APITestCase -from service_catalog.models import Operation +from service_catalog.models import Operation, OperationType from tests.setup import SetupServiceCommon @@ -23,6 +23,13 @@ def setUp(self): job_template=self.job_template_1, process_timeout_second=30 ) + self.operation_update_1 = Operation.objects.create( + name="Operation 3 (Update)", + type=OperationType.UPDATE, + service=self.service_2, + job_template=self.job_template_1, + process_timeout_second=30 + ) print("SetupOperationCommon finished") diff --git a/tests/test_plugins/survey_validators_test/__init__.py b/tests/test_plugins/survey_validators_test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_plugins/survey_validators_test/survey_test.py b/tests/test_plugins/survey_validators_test/survey_test.py new file mode 100644 index 000000000..1a5b0eb7f --- /dev/null +++ b/tests/test_plugins/survey_validators_test/survey_test.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import User + +from profiles.models import Scope +from service_catalog.forms import SurveyValidator +from service_catalog.models import Operation, Instance + + +class Validator1(SurveyValidator): + def validate_survey(self): + if self.survey.get('ram') == 0 and self.survey.get('vcpu') == 0: + self.fail("ram and vCPU are both equal to 0") + if self.survey.get('vcpu') == 0: + self.fail("vCPU is equal to 0", "vcpu") + + +class Validator2(SurveyValidator): + def validate_survey(self): + print("everything is good") + + +class ValidatorDay1(SurveyValidator): + def validate_survey(self): + assert self.survey.get("ram") == 1 + assert self.survey.get("vcpu") == 1 + assert self.survey["request_comment"] == "comment day1" + assert self.user == User.objects.get(username="superuser") + assert self.operation == Operation.objects.get(name="Operation 1 (Create)") + assert self.instance.quota_scope == Scope.objects.get(name="Organization 1") + assert self.instance.name == "instance test" + assert self.instance.id == None + self.fail("Everything is good, it's just a message to be sure that code was executed") + + +class ValidatorDay2(SurveyValidator): + def validate_survey(self): + assert self.survey.get("ram") == 1 + assert self.survey.get("vcpu") == 1 + assert self.survey["request_comment"] == "comment day2" + assert self.user == User.objects.get(username="superuser") + assert self.operation == Operation.objects.get(name="Operation 3 (Update)") + assert self.instance.quota_scope == Scope.objects.get(name="Organization 1") + assert self.instance.name == "Instance 1 - Org 1" + assert self.instance.id != None + assert self.instance == Instance.objects.get(name="Instance 1 - Org 1") + self.fail("Everything is good, it's just a message to be sure that code was executed") diff --git a/tests/test_plugins/test_plugin_controller.py b/tests/test_plugins/test_plugin_controller.py index 7c06457d4..53a482766 100644 --- a/tests/test_plugins/test_plugin_controller.py +++ b/tests/test_plugins/test_plugin_controller.py @@ -5,14 +5,16 @@ class TestPluginController(TestCase): - + ###################################################### + # Field validators + ###################################################### @override_settings(FIELD_VALIDATOR_PATH="tests/test_plugins/field_validators_test") - def test_get_user_provisioned_validators(self): + def test_get_user_provisioned_field_validators(self): expected_list = [ "even_number", "superior_to_10" ] - self.assertEqual(expected_list, PluginController.get_user_provisioned_validators()) + self.assertEqual(expected_list, PluginController.get_user_provisioned_field_validators()) @override_settings(FIELD_VALIDATOR_PATH="tests/test_plugins/field_validators_test") def test_get_ui_field_validator_def(self): @@ -35,3 +37,29 @@ def test_get_api_field_validator_def(self): @override_settings(FIELD_VALIDATOR_PATH="tests/test_plugins/field_validators_test") def test_get_ui_field_validator_def_return_none_if_wrong_file(self): self.assertIsNone(PluginController.get_api_field_validator_def("does_not_exist")) + + ###################################################### + # Survey validators + ###################################################### + @override_settings(SURVEY_VALIDATOR_PATH="tests/test_plugins/survey_validators_test") + def test_get_user_provisioned_survey_validators(self): + expected_list = [ + 'survey_test.Validator1', + 'survey_test.Validator2', + 'survey_test.ValidatorDay1', + 'survey_test.ValidatorDay2' + ] + + self.assertCountEqual(expected_list, PluginController.get_user_provisioned_survey_validators()) + + @override_settings(SURVEY_VALIDATOR_PATH="tests/test_plugins/survey_validators_test") + def test_get_survey_validator_def(self): + survey_validator = PluginController.get_survey_validator_def("survey_test.Validator1") + with self.assertRaises(serializers.ValidationError): + survey_validator(survey={'vcpu': 0, 'ram': 0}, user=1, operation=2, instance=3)._validate() + + survey_validator(survey={}, user=1, operation=2, instance=3)._validate() + + @override_settings(SURVEY_VALIDATOR_PATH="tests/test_plugins/survey_validators_test") + def test_get_survey_validator_def_return_none_if_wrong_file(self): + self.assertIsNone(PluginController.get_survey_validator_def("does_not_exist")) diff --git a/tests/test_service_catalog/test_api/test_request/test_operation_request_create.py b/tests/test_service_catalog/test_api/test_request/test_operation_request_create.py index c7902e31f..6f0a2b37e 100644 --- a/tests/test_service_catalog/test_api/test_request/test_operation_request_create.py +++ b/tests/test_service_catalog/test_api/test_request/test_operation_request_create.py @@ -1,7 +1,7 @@ from rest_framework import status from rest_framework.reverse import reverse -from service_catalog.models import Request +from service_catalog.models import Request, InstanceState from tests.test_service_catalog.base_test_request import BaseTestRequestAPI @@ -9,6 +9,8 @@ class TestApiOperationRequestCreate(BaseTestRequestAPI): def setUp(self): super(TestApiOperationRequestCreate, self).setUp() + self.test_instance.state = InstanceState.AVAILABLE + self.test_instance.save() self.kwargs = { "instance_id": self.test_instance.id, "operation_id": self.update_operation_test.id, @@ -19,7 +21,7 @@ def setUp(self): 'text_variable': 'my text' } } - self.expected = {'text_variable': 'my text'} + self.expected = {'text_variable': 'my text', 'request_comment': None} def test_can_create(self): request_count = Request.objects.count() diff --git a/tests/test_service_catalog/test_api/test_serializers/test_survey_validator.py b/tests/test_service_catalog/test_api/test_serializers/test_survey_validator.py new file mode 100644 index 000000000..f23a57f03 --- /dev/null +++ b/tests/test_service_catalog/test_api/test_serializers/test_survey_validator.py @@ -0,0 +1,146 @@ +from django.contrib.auth.models import User + +from service_catalog.api.serializers import ServiceRequestSerializer, OperationRequestSerializer +from service_catalog.models import InstanceState +from tests.setup import SetupInstance + +from django.test import override_settings + + +class TestSurveyValidatorDay1Serializer(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + self.operation_create_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_create_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_create_1.validators = 'survey_test.ValidatorDay1' + self.operation_create_1.save() + data = { + "squest_instance_name": "instance test", + "quota_scope": self.org1.id, + 'request_comment': 'comment day1', + "fill_in_survey": { + "vcpu": 1, + "ram": 1 + } + } + serializer = ServiceRequestSerializer(operation=self.operation_create_1, user=self.superuser, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors['__all__'][0], + "Everything is good, it's just a message to be sure that code was executed") + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_0_0(self): + data = { + "squest_instance_name": "instance test", + "quota_scope": self.org1.id, + "request_comment": "None", + "fill_in_survey": { + "vcpu": 0, + "ram": 0 + } + } + serializer = ServiceRequestSerializer(operation=self.operation_create_1, user=self.superuser, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors['__all__'][0], 'ram and vCPU are both equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_ram_1_and_cpu_0(self): + data = { + "squest_instance_name": "instance test", + "quota_scope": self.org1.id, + "request_comment": "None", + "fill_in_survey": { + "vcpu": 0, + "ram": 1 + } + } + serializer = ServiceRequestSerializer(operation=self.operation_create_1, user=self.superuser, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors['vcpu'][0], 'vCPU is equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_success_with_1_1(self): + data = { + "squest_instance_name": "instance test", + "quota_scope": self.org1.id, + "request_comment": "None", + "fill_in_survey": { + "vcpu": 1, + "ram": 1 + } + } + serializer = ServiceRequestSerializer(operation=self.operation_create_1, user=self.superuser, data=data) + self.assertTrue(serializer.is_valid()) + + +class TestSurveyValidatorDay2Serializer(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + self.operation_update_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_update_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + self.instance_1_org1.state = InstanceState.AVAILABLE + self.instance_1_org1.service = self.operation_update_1.service + self.instance_1_org1.save() + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_update_1.validators = 'survey_test.ValidatorDay2' + self.operation_update_1.save() + data = { + 'request_comment': 'comment day2', + "fill_in_survey": { + "vcpu": 1, + "ram": 1 + } + } + form = OperationRequestSerializer(operation=self.operation_update_1, instance=self.instance_1_org1, + user=self.superuser, data=data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['__all__'][0], + "Everything is good, it's just a message to be sure that code was executed") + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_0_0(self): + data = { + "fill_in_survey": { + "vcpu": 0, + "ram": 0 + } + } + form = OperationRequestSerializer(operation=self.operation_update_1, instance=self.instance_1_org1, + user=self.superuser, data=data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['__all__'][0], 'ram and vCPU are both equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_ram_1_and_cpu_0(self): + data = { + "fill_in_survey": { + "vcpu": 0, + "ram": 1 + } + } + form = OperationRequestSerializer(operation=self.operation_update_1, instance=self.instance_1_org1, + user=self.superuser, data=data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['vcpu'][0], 'vCPU is equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_success_with_1_1(self): + data = { + "fill_in_survey": { + "vcpu": 1, + "ram": 1 + } + } + form = OperationRequestSerializer(operation=self.operation_update_1, instance=self.instance_1_org1, + user=self.superuser, data=data) + self.assertTrue(form.is_valid()) diff --git a/tests/test_service_catalog/test_api/test_views/test_survey_validator.py b/tests/test_service_catalog/test_api/test_views/test_survey_validator.py new file mode 100644 index 000000000..e5b34627e --- /dev/null +++ b/tests/test_service_catalog/test_api/test_views/test_survey_validator.py @@ -0,0 +1,83 @@ +import json +from django.contrib.auth.models import User +from django.urls import reverse +from rest_framework import status + +from service_catalog.models import InstanceState +from tests.setup import SetupInstanceAPI + +from django.test import override_settings + + +class TestSurveyValidatorDay1API(SetupInstanceAPI): + + def setUp(self): + SetupInstanceAPI.setUp(self) + self.operation_create_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_create_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + url_kwargs = { + 'service_id': self.operation_create_1.service.id, + 'pk': self.operation_create_1.id, + } + self.url = reverse('api_service_request_create', kwargs=url_kwargs) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_create_1.validators = 'survey_test.ValidatorDay1' + self.operation_create_1.save() + data = { + 'squest_instance_name': 'instance test', + 'request_comment': 'comment day1', + 'quota_scope': self.org1.id, + 'fill_in_survey': { + 'vcpu': 1, + 'ram': 1 + }, + } + response = self.client.post(self.url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + loaded_content = json.loads(response.content) + self.assertEqual( + loaded_content, + {'__all__': ["Everything is good, it's just a message to be sure that code was executed"]} + ) + + +class TestSurveyValidatorDay2API(SetupInstanceAPI): + + def setUp(self): + SetupInstanceAPI.setUp(self) + self.operation_update_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_update_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + self.instance_1_org1.state = InstanceState.AVAILABLE + self.instance_1_org1.service = self.operation_update_1.service + self.instance_1_org1.save() + url_kwargs = { + 'instance_id': self.instance_1_org1.id, + 'operation_id': self.operation_update_1.id, + } + + self.url = reverse('api_operation_request_create', kwargs=url_kwargs) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_update_1.validators = 'survey_test.ValidatorDay2' + self.operation_update_1.save() + data = { + 'request_comment': 'comment day2', + 'fill_in_survey': { + 'vcpu': 1, + 'ram': 1 + }, + } + response = self.client.post(self.url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + loaded_content = json.loads(response.content) + self.assertEqual( + loaded_content, + {'__all__': ["Everything is good, it's just a message to be sure that code was executed"]} + ) diff --git a/tests/test_service_catalog/test_forms/test_survey_validator.py b/tests/test_service_catalog/test_forms/test_survey_validator.py new file mode 100644 index 000000000..8151d1e72 --- /dev/null +++ b/tests/test_service_catalog/test_forms/test_survey_validator.py @@ -0,0 +1,126 @@ +from django.contrib.auth.models import User + +from profiles.models import Scope +from service_catalog.forms import ServiceRequestForm, OperationRequestForm +from service_catalog.models import InstanceState +from tests.setup import SetupInstance + +from django.test import override_settings + + +class TestSurveyValidatorDay1Form(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + self.operation_create_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_create_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + self.parameters = { + 'operation': self.operation_create_1, + 'quota_scope': Scope.objects.get(id=self.org1.id), + 'user': self.superuser, + 'instance_name': "instance test" + } + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_create_1.validators = 'survey_test.ValidatorDay1' + self.operation_create_1.save() + data = { + 'request_comment': 'comment day1', + 'vcpu': 1, + 'ram': 1 + } + form = ServiceRequestForm(data, **self.parameters) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['__all__'].data[0].message, + "Everything is good, it's just a message to be sure that code was executed") + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_0_0(self): + data = { + 'vcpu': 0, + 'ram': 0 + } + form = ServiceRequestForm(data, **self.parameters) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['__all__'].data[0].message, 'ram and vCPU are both equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_ram_1_and_cpu_0(self): + data = { + 'vcpu': 0, + 'ram': 1 + } + form = ServiceRequestForm(data, **self.parameters) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['vcpu'].data[0].message, 'vCPU is equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_success_with_1_1(self): + data = { + 'vcpu': 1, + 'ram': 1 + } + form = ServiceRequestForm(data, **self.parameters) + self.assertTrue(form.is_valid()) + + +class TestSurveyValidatorDay2Form(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + self.operation_update_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_update_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + self.instance_1_org1.state = InstanceState.AVAILABLE + self.instance_1_org1.save() + self.parameters = { + 'instance': self.instance_1_org1, + 'operation': self.operation_update_1 + } + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_update_1.validators = 'survey_test.ValidatorDay2' + self.operation_update_1.save() + data = { + 'request_comment': 'comment day2', + 'vcpu': 1, + 'ram': 1 + } + form = OperationRequestForm(user=self.superuser, data=data, **self.parameters) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['__all__'].data[0].message, + "Everything is good, it's just a message to be sure that code was executed") + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_0_0(self): + data = { + 'vcpu': 0, + 'ram': 0 + } + form = OperationRequestForm(user=self.superuser, data=data, **self.parameters) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['__all__'].data[0].message, 'ram and vCPU are both equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_fail_with_ram_1_and_cpu_0(self): + data = { + 'vcpu': 0, + 'ram': 1 + } + form = OperationRequestForm(user=self.superuser, data=data, **self.parameters) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['vcpu'].data[0].message, 'vCPU is equal to 0') + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_success_with_1_1(self): + data = { + 'vcpu': 1, + 'ram': 1 + } + form = OperationRequestForm(user=self.superuser, data=data, **self.parameters) + self.assertTrue(form.is_valid()) diff --git a/tests/test_service_catalog/test_models/test_survey_validator.py b/tests/test_service_catalog/test_models/test_survey_validator.py new file mode 100644 index 000000000..dff1d2515 --- /dev/null +++ b/tests/test_service_catalog/test_models/test_survey_validator.py @@ -0,0 +1,159 @@ +import json +from django.contrib.auth.models import User +from django.urls import reverse +from rest_framework import status + +from service_catalog.api.serializers import ServiceRequestSerializer, OperationRequestSerializer +from service_catalog.models import InstanceState +from tests.setup import SetupInstanceAPI, SetupInstance + +from django.test import override_settings + + +class TestSurveyValidatorDay1API(SetupInstanceAPI): + + def setUp(self): + SetupInstanceAPI.setUp(self) + self.operation_create_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_create_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + url_kwargs = { + 'service_id': self.operation_create_1.service.id, + 'pk': self.operation_create_1.id, + } + self.url = reverse('api_service_request_create', kwargs=url_kwargs) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_create_1.validators = 'survey_test.ValidatorDay1' + self.operation_create_1.save() + data = { + 'squest_instance_name': 'instance test', + 'request_comment': 'comment day1', + 'quota_scope': self.org1.id, + 'fill_in_survey': { + 'vcpu': 1, + 'ram': 1 + }, + } + response = self.client.post(self.url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + loaded_content = json.loads(response.content) + self.assertEqual( + loaded_content, + {'__all__': ["Everything is good, it's just a message to be sure that code was executed"]} + ) + + + + + + +class TestSurveyValidatorDay2API(SetupInstanceAPI): + + def setUp(self): + SetupInstanceAPI.setUp(self) + self.operation_update_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_update_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + self.instance_1_org1.state = InstanceState.AVAILABLE + self.instance_1_org1.service = self.operation_update_1.service + self.instance_1_org1.save() + url_kwargs = { + 'instance_id': self.instance_1_org1.id, + 'operation_id': self.operation_update_1.id, + } + + self.url = reverse('api_operation_request_create', kwargs=url_kwargs) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_update_1.validators = 'survey_test.ValidatorDay2' + self.operation_update_1.save() + data = { + 'request_comment': 'comment day2', + 'fill_in_survey': { + 'vcpu': 1, + 'ram': 1 + }, + } + response = self.client.post(self.url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + loaded_content = json.loads(response.content) + self.assertEqual( + loaded_content, + {'__all__': ["Everything is good, it's just a message to be sure that code was executed"]} + ) + +class TestSurveyValidatorDay1UI(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + + self.operation_create_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_create_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + args = { + "service_id": self.operation_create_1.service.id, + "operation_id": self.operation_create_1.id + } + self.url = reverse('service_catalog:request_service', kwargs=args) + + data_form1 = { + "0-name": "instance test", + "0-quota_scope": self.org1.id, + "service_request_wizard_view-current_step": "0", + } + + response = self.client.post(self.url, data=data_form1) + self.assertEqual(response.status_code, 200) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_create_1.validators = 'survey_test.ValidatorDay1' + self.operation_create_1.save() + data_form2 = { + '1-request_comment': 'comment day1', + "1-ram": 1, + "1-vcpu": 1, + "service_request_wizard_view-current_step": "1", + } + response = self.client.post(self.url, data=data_form2) + self.assertEqual(200, response.status_code) + self.assertEqual(response.context["form"].errors["__all__"][0], + "Everything is good, it's just a message to be sure that code was executed") + +class TestSurveyValidatorDay2UI(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + + self.operation_update_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_update_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + self.instance_1_org1.state = InstanceState.AVAILABLE + self.instance_1_org1.service = self.operation_update_1.service + self.instance_1_org1.save() + args = { + 'instance_id': self.instance_1_org1.id, + 'operation_id': self.operation_update_1.id + } + self.url = reverse('service_catalog:instance_request_new_operation', kwargs=args) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_update_1.validators = 'survey_test.ValidatorDay2' + self.operation_update_1.save() + data = { + 'request_comment': 'comment day2', + "vcpu": 1, + "ram": 1 + } + response = self.client.post(self.url, data=data) + self.assertEqual(200, response.status_code) + self.assertEqual(response.context["form"].errors["__all__"][0], + "Everything is good, it's just a message to be sure that code was executed") diff --git a/tests/test_service_catalog/test_serializers/__init__.py b/tests/test_service_catalog/test_serializers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_service_catalog/test_views/test_common/test_survey_validator.py b/tests/test_service_catalog/test_views/test_common/test_survey_validator.py new file mode 100644 index 000000000..cc0c9128a --- /dev/null +++ b/tests/test_service_catalog/test_views/test_common/test_survey_validator.py @@ -0,0 +1,79 @@ +from django.contrib.auth.models import User +from django.urls import reverse + +from service_catalog.models import InstanceState +from tests.setup import SetupInstance + +from django.test import override_settings + + +class TestSurveyValidatorDay1UI(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + + self.operation_create_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_create_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + args = { + "service_id": self.operation_create_1.service.id, + "operation_id": self.operation_create_1.id + } + self.url = reverse('service_catalog:request_service', kwargs=args) + + data_form1 = { + "0-name": "instance test", + "0-quota_scope": self.org1.id, + "service_request_wizard_view-current_step": "0", + } + + response = self.client.post(self.url, data=data_form1) + self.assertEqual(response.status_code, 200) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_create_1.validators = 'survey_test.ValidatorDay1' + self.operation_create_1.save() + data_form2 = { + '1-request_comment': 'comment day1', + "1-ram": 1, + "1-vcpu": 1, + "service_request_wizard_view-current_step": "1", + } + response = self.client.post(self.url, data=data_form2) + self.assertEqual(200, response.status_code) + self.assertEqual(response.context["form"].errors["__all__"][0], + "Everything is good, it's just a message to be sure that code was executed") + +class TestSurveyValidatorDay2UI(SetupInstance): + + def setUp(self): + SetupInstance.setUp(self) + + self.operation_update_1.validators = 'survey_test.Validator1,survey_test.Validator2' + self.operation_update_1.save() + self.superuser = User.objects.create_superuser(username='superuser') + self.client.force_login(user=self.superuser) + self.instance_1_org1.state = InstanceState.AVAILABLE + self.instance_1_org1.service = self.operation_update_1.service + self.instance_1_org1.save() + args = { + 'instance_id': self.instance_1_org1.id, + 'operation_id': self.operation_update_1.id + } + self.url = reverse('service_catalog:instance_request_new_operation', kwargs=args) + + @override_settings(SURVEY_VALIDATOR_PATH='tests/test_plugins/survey_validators_test') + def test_injected_data_are_good(self): + self.operation_update_1.validators = 'survey_test.ValidatorDay2' + self.operation_update_1.save() + data = { + 'request_comment': 'comment day2', + "vcpu": 1, + "ram": 1 + } + response = self.client.post(self.url, data=data) + self.assertEqual(200, response.status_code) + self.assertEqual(response.context["form"].errors["__all__"][0], + "Everything is good, it's just a message to be sure that code was executed")