diff --git a/development/configuration.py b/development/configuration.py index 1083cd6..e32a3c4 100644 --- a/development/configuration.py +++ b/development/configuration.py @@ -1,5 +1,4 @@ import os -import socket from .configuration_example import * @@ -69,4 +68,8 @@ class ContainsAll: def __contains__(self, v): return True + INTERNAL_IPS = ContainsAll() + + +CUSTOM_VALIDATORS = {"core.datasource": ["validity.custom_validators.DataSourceValidator"]} diff --git a/validity/custom_validators.py b/validity/custom_validators.py new file mode 100644 index 0000000..98ef688 --- /dev/null +++ b/validity/custom_validators.py @@ -0,0 +1,17 @@ +from extras.validators import CustomValidator + + +class DataSourceValidator(CustomValidator): + """ + This validator prevents creation of more than one Data Source with "device_config_default" mark set to True + """ + + def validate(self, instance): + DataSource = type(instance) + if DataSource.__name__ != "DataSource": + return + if not instance.cf.get("device_config_default"): + return + default_datasources = DataSource.objects.filter(custom_field_data__device_config_default=True) + if default_datasources.exclude(pk=instance.pk).count() > 0: + self.fail("Default repository already exists", field="cf_device_config_default") diff --git a/validity/models/base.py b/validity/models/base.py index 37b2854..64046fa 100644 --- a/validity/models/base.py +++ b/validity/models/base.py @@ -1,5 +1,6 @@ import logging +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -40,9 +41,23 @@ class DataSourceMixin(models.Model): class Meta: abstract = True - def _validate_db_or_git_filled(self) -> bool: # TODO: add this to self.clean + @property + def _validate_db_or_git_filled(self) -> bool: return True + def clean(self) -> None: + text_value = getattr(self, self.text_db_field_name) + if text_value and (self.data_source or self.data_file): + raise ValidationError(_(f"You cannot set both: data_source/data_file and {self.text_db_field_name}")) + if self._validate_db_or_git_filled and not text_value and (not self.data_source or not self.data_file): + raise ValidationError( + { + self.text_db_field_name: _( + f"You must set either {self.text_db_field_name} or both: data_source and data_file" + ) + } + ) + def effective_text_field(self) -> str: text_db_value = getattr(self, self.text_db_field_name) if text_db_value: @@ -69,7 +84,6 @@ class BaseReadOnlyModel( WebhooksMixin, models.Model, ): - created = models.DateTimeField(auto_now_add=True, blank=True, null=True) last_updated = models.DateTimeField(auto_now=True, blank=True, null=True) diff --git a/validity/models/serializer.py b/validity/models/serializer.py index 2f81275..3877060 100644 --- a/validity/models/serializer.py +++ b/validity/models/serializer.py @@ -27,6 +27,7 @@ def __str__(self) -> str: def get_extraction_method_color(self): return ConfigExtractionChoices.colors.get(self.extraction_method) + @property def _validate_db_or_git_filled(self) -> bool: return self.extraction_method == "TTP" diff --git a/validity/tests/test_custom_validators.py b/validity/tests/test_custom_validators.py new file mode 100644 index 0000000..a6423bc --- /dev/null +++ b/validity/tests/test_custom_validators.py @@ -0,0 +1,21 @@ +from functools import partial + +import pytest +from core.models import DataSource +from django.core.exceptions import ValidationError + +from validity.custom_validators import DataSourceValidator + + +@pytest.mark.django_db +def test_device_validator(create_custom_fields): + validator = DataSourceValidator() + GitDataSource = partial(DataSource, source_url="http://ab.io/d", type="git") + data_source1 = GitDataSource(name="ds1", custom_field_data={"device_config_default": True}) + validator.validate(data_source1) + data_source1.save() + data_source2 = GitDataSource(name="ds2", custom_field_data={"device_config_default": True}) + with pytest.raises(ValidationError): + validator.validate(data_source2) + data_source3 = GitDataSource(name="ds3", custom_field_data={"device_config_default": False}) + validator.validate(data_source3) diff --git a/validity/tests/test_models/_test_clean.py b/validity/tests/test_models/_test_clean.py deleted file mode 100644 index 080ec79..0000000 --- a/validity/tests/test_models/_test_clean.py +++ /dev/null @@ -1,87 +0,0 @@ -import textwrap - -import pytest -from django.core.exceptions import ValidationError -from factories import CompTestGitFactory, GitRepoFactory, NameSetGitFactory, SelectorFactory, SerializerGitFactory - - -@pytest.mark.django_db -def test_only_one_default_repo(): - GitRepoFactory(default=True) - with pytest.raises(ValidationError): - repo = GitRepoFactory.build(default=True) - repo.clean() - - -class BaseTestClean: - factory: type - right_kwargs: list[dict] = [] - wrong_kwargs: list[dict] - - @pytest.mark.django_db - def test_clean_right(self, subtests): - for i, kwargs in enumerate(self.right_kwargs): - with subtests.test(id=i): - model = self.factory(**kwargs) - model.clean() - - @pytest.mark.django_db - def test_clean_wrong(self, subtests): - for i, kwargs in enumerate(self.wrong_kwargs): - with subtests.test(id=i): - model = self.factory(**kwargs) - with pytest.raises(ValidationError): - model.clean() - - -class TestNameSet(BaseTestClean): - factory = NameSetGitFactory - right_definition = textwrap.dedent( - """ - from collections import Counter - - __all__ = ['A', 'Counter', 'func'] - - def func(): - pass - - class A: - pass - """ - ) - - right_kwargs = [{"definitions": right_definition, "repo": None, "file_path": ""}] - wrong_kwargs = [ - {"definitions": "a = 10", "repo": None, "file_path": ""}, - {"definitions": "some invalid syntax", "repo": None, "file_path": ""}, - {"definitions": "def some_func(): pass", "repo": None, "file_path": ""}, - {"definitions": right_definition, "repo": None}, - {"definitions": right_definition, "file_path": ""}, - ] - - -class TestSelector(BaseTestClean): - factory = SelectorFactory - - wrong_kwargs = [{"name_filter": "qwerty", "dynamic_pairs": "NAME"}, {"name_filter": "invalidregex))))"}] - - -class TestSerializer(BaseTestClean): - factory = SerializerGitFactory - - right_kwargs = [ - {"extraction_method": "TTP", "ttp_template": "interface {{ interface }}", "repo": None, "file_path": ""}, - {"extraction_method": "YAML", "repo": None, "file_path": ""}, - ] - wrong_kwargs = [ - {"extraction_method": "TTP", "ttp_template": "", "repo": None, "file_path": ""}, - {"extraction_method": "TTP", "ttp_template": "qwerty", "file_path": ""}, - {"extraction_method": "YAML"}, - ] - - -class TestTest(BaseTestClean): - factory = CompTestGitFactory - - right_kwargs = [{"expression": "a==1", "repo": None, "file_path": ""}, {}] - wrong_kwargs = [{"expression": "a==b", "repo": None}, {"expression": "a = 10 + 15", "repo": None, "file_path": ""}] diff --git a/validity/tests/test_models/test_clean.py b/validity/tests/test_models/test_clean.py new file mode 100644 index 0000000..8c30fbd --- /dev/null +++ b/validity/tests/test_models/test_clean.py @@ -0,0 +1,97 @@ +import textwrap + +import pytest +from django.core.exceptions import ValidationError +from factories import CompTestDSFactory, NameSetDSFactory, SelectorFactory, SerializerDSFactory + + +class BaseTestClean: + factory: type + right_kwargs: list[dict] = [] + wrong_kwargs: list[dict] + + @pytest.mark.django_db + def test_clean_right(self, subtests): + for i, kwargs in enumerate(self.right_kwargs): + with subtests.test(id=i): + model = self.factory(**kwargs) + model.clean() + + @pytest.mark.django_db + def test_clean_wrong(self, subtests): + for i, kwargs in enumerate(self.wrong_kwargs): + with subtests.test(id=i): + model = self.factory(**kwargs) + with pytest.raises(ValidationError): + model.clean() + + +class TestDBNameSet(BaseTestClean): + factory = NameSetDSFactory + right_definition = textwrap.dedent( + """ + from collections import Counter + + __all__ = ['A', 'Counter', 'func'] + + def func(): + pass + + class A: + pass + """ + ) + + right_kwargs = [ + {"definitions": right_definition, "data_source": None, "data_file": None}, + {"definitions": "", "contents": right_definition}, + ] + wrong_kwargs = [ + {"definitions": "a = 10", "data_source": None, "data_file": None}, + {"definitions": "some invalid syntax", "data_source": None, "data_file": None}, + {"definitions": "def some_func(): pass", "data_source": None, "data_file": None}, + {"definitions": right_definition, "data_source": None}, + {"definitions": right_definition, "data_file": None}, + ] + + +class TestSelector(BaseTestClean): + factory = SelectorFactory + + wrong_kwargs = [{"name_filter": "qwerty", "dynamic_pairs": "NAME"}, {"name_filter": "invalidregex))))"}] + + +class TestSerializer(BaseTestClean): + factory = SerializerDSFactory + + right_kwargs = [ + { + "extraction_method": "TTP", + "ttp_template": "interface {{ interface }}", + "data_source": None, + "data_file": None, + }, + {"extraction_method": "YAML", "data_source": None, "data_file": None}, + {"extraction_method": "TTP", "ttp_template": "", "contents": "interface {{ interface }}"}, + ] + wrong_kwargs = [ + {"extraction_method": "TTP", "ttp_template": "", "data_source": None, "data_file": None}, + {"extraction_method": "TTP", "ttp_template": "qwerty"}, + {"extraction_method": "TTP", "ttp_template": "qwerty", "data_source": None}, + {"extraction_method": "TTP", "ttp_template": "qwerty", "data_file": None}, + {"extraction_method": "YAML"}, + ] + + +class TestCompTest(BaseTestClean): + factory = CompTestDSFactory + + right_kwargs = [{"expression": "a==1", "data_source": None, "data_file": None}, {}] + wrong_kwargs = [ + {"expression": "a == b"}, + {"expression": "", "data_source": None, "data_file": None}, + {"expression": "a==b", "data_source": None}, + {"expression": "a==b", "data_file": None}, + {"expression": "a = 10 + 15", "data_source": None, "data_file": None}, + {"expression": "import itertools; a==b", "data_source": None, "data_file": None}, + ]