From 5c743df7946125b38aea69ae43b3dc31e93732e3 Mon Sep 17 00:00:00 2001 From: Bruno Bord Date: Tue, 13 Feb 2018 12:34:34 +0100 Subject: [PATCH 1/2] Adding tests on the `formidable.yml` file definition This YAML file defines the accepted methods and formats for the form builder. It appeared that this is incomplete or obsolete and requires to be tested. As soon as something changes in this form definition, we should reflect it using a test that shows the impact. It is recommended to use the TDD method, by writing the test first and implement the change on your second move. The present test files are reflecting the spec *as it is written now* and will probably evolve as we'll fix them regarding the Python code that is supposed to implement this spec. --- CHANGELOG.rst | 2 +- docs/setup.cfg | 3 + docs/tests/__init__.py | 52 +++ docs/tests/fixtures/0000_empty.json | 1 + docs/tests/fixtures/0001_just_id.json | 3 + docs/tests/fixtures/0002_id_label.json | 4 + docs/tests/fixtures/0003_id_not_integer.json | 5 + .../fixtures/0004_id_label_description.json | 5 + .../fixtures/0010_wrong_type_field_dict.json | 6 + .../fixtures/0010_wrong_type_field_int.json | 6 + docs/tests/fixtures/0011_empty_fields.json | 6 + .../fixtures/0012_fields_empty_object.json | 8 + docs/tests/fixtures/0012_fields_ok.json | 15 + .../fixtures/0013_fields_placeholder.json | 16 + docs/tests/fixtures/0014_fields_multiple.json | 17 + docs/tests/fixtures/0015_fields_items.json | 21 ++ docs/tests/fixtures/0016_fields_accesses.json | 20 ++ .../fixtures/0017_fields_validations.json | 20 ++ docs/tests/fixtures/0018_fields_defaults.json | 17 + .../tests/fixtures/0020_simple_condition.json | 60 ++++ docs/tests/test_check_schema.py | 7 + docs/tests/test_fields.py | 340 ++++++++++++++++++ docs/tests/test_forms.py | 52 +++ docs/tests/test_forms_conditions.py | 153 ++++++++ tox.ini | 13 + 25 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 docs/setup.cfg create mode 100644 docs/tests/__init__.py create mode 100644 docs/tests/fixtures/0000_empty.json create mode 100644 docs/tests/fixtures/0001_just_id.json create mode 100644 docs/tests/fixtures/0002_id_label.json create mode 100644 docs/tests/fixtures/0003_id_not_integer.json create mode 100644 docs/tests/fixtures/0004_id_label_description.json create mode 100644 docs/tests/fixtures/0010_wrong_type_field_dict.json create mode 100644 docs/tests/fixtures/0010_wrong_type_field_int.json create mode 100644 docs/tests/fixtures/0011_empty_fields.json create mode 100644 docs/tests/fixtures/0012_fields_empty_object.json create mode 100644 docs/tests/fixtures/0012_fields_ok.json create mode 100644 docs/tests/fixtures/0013_fields_placeholder.json create mode 100644 docs/tests/fixtures/0014_fields_multiple.json create mode 100644 docs/tests/fixtures/0015_fields_items.json create mode 100644 docs/tests/fixtures/0016_fields_accesses.json create mode 100644 docs/tests/fixtures/0017_fields_validations.json create mode 100644 docs/tests/fixtures/0018_fields_defaults.json create mode 100644 docs/tests/fixtures/0020_simple_condition.json create mode 100644 docs/tests/test_check_schema.py create mode 100644 docs/tests/test_fields.py create mode 100644 docs/tests/test_forms.py create mode 100644 docs/tests/test_forms_conditions.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6d23b71d..bd828100 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ ChangeLog master (unreleased) =================== -Nothing here yet. +- Added tests against the ``formidable.yml`` schema definition of Forms (#295). Release 1.3.0 (2018-02-14) ========================== diff --git a/docs/setup.cfg b/docs/setup.cfg new file mode 100644 index 00000000..1bc5e806 --- /dev/null +++ b/docs/setup.cfg @@ -0,0 +1,3 @@ +[tool:pytest] +; intentionally left blank +; it voids the defaults that live in the setup.cfg file at the root of the repository. diff --git a/docs/tests/__init__.py b/docs/tests/__init__.py new file mode 100644 index 00000000..23e44956 --- /dev/null +++ b/docs/tests/__init__.py @@ -0,0 +1,52 @@ +from os.path import abspath, join, dirname +import json +import collections + +from jsonschema import Draft4Validator +import yaml + +ROOT_PATH = dirname(abspath(__file__)) + + +def yaml2json(): + """ + Builds a dictionary of the BuilderForm definition out of the Swagger YAML. + """ + # Setup support for ordered dicts so we do not lose ordering + # when importing from YAML + _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG + + def dict_representer(dumper, data): + return dumper.represent_mapping(_mapping_tag, data.iteritems()) + + def dict_constructor(loader, node): + return collections.OrderedDict(loader.construct_pairs(node)) + + yaml.add_representer(collections.OrderedDict, dict_representer) + yaml.add_constructor(_mapping_tag, dict_constructor) + + # Building raw data out of the Swagger Formidable definition + swagger_file = join(ROOT_PATH, '..', 'swagger', 'formidable.yml') + with open(swagger_file) as fd: + data = yaml.load(fd) + + to_exclude = ( + 'BuilderForm', 'InputError', 'BuilderError', 'InputForm', 'InputField' + ) + definitions = (item for item in data['definitions'].items()) + definitions = filter(lambda item: item[0] not in to_exclude, definitions) + + new_data = {} + new_data.update(data['definitions']['BuilderForm']) + new_data.update({"definitions": dict(definitions)}) + return new_data + + +def _load_fixture(path): + if not path.startswith('fixtures/'): + path = 'fixtures/{}'.format(path) + return json.load(open(join(ROOT_PATH, path))) + + +schema = yaml2json() +validator = Draft4Validator(schema) diff --git a/docs/tests/fixtures/0000_empty.json b/docs/tests/fixtures/0000_empty.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/docs/tests/fixtures/0000_empty.json @@ -0,0 +1 @@ +{} diff --git a/docs/tests/fixtures/0001_just_id.json b/docs/tests/fixtures/0001_just_id.json new file mode 100644 index 00000000..2572ae5f --- /dev/null +++ b/docs/tests/fixtures/0001_just_id.json @@ -0,0 +1,3 @@ +{ + "id": 1 +} diff --git a/docs/tests/fixtures/0002_id_label.json b/docs/tests/fixtures/0002_id_label.json new file mode 100644 index 00000000..03d390e0 --- /dev/null +++ b/docs/tests/fixtures/0002_id_label.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "label": "This is my form title" +} diff --git a/docs/tests/fixtures/0003_id_not_integer.json b/docs/tests/fixtures/0003_id_not_integer.json new file mode 100644 index 00000000..3003cf04 --- /dev/null +++ b/docs/tests/fixtures/0003_id_not_integer.json @@ -0,0 +1,5 @@ +{ + "id": "hello", + "label": "This is my form title", + "description": "Here's the description of my form." +} diff --git a/docs/tests/fixtures/0004_id_label_description.json b/docs/tests/fixtures/0004_id_label_description.json new file mode 100644 index 00000000..ee2e9825 --- /dev/null +++ b/docs/tests/fixtures/0004_id_label_description.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form." +} diff --git a/docs/tests/fixtures/0010_wrong_type_field_dict.json b/docs/tests/fixtures/0010_wrong_type_field_dict.json new file mode 100644 index 00000000..429d507b --- /dev/null +++ b/docs/tests/fixtures/0010_wrong_type_field_dict.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": {"hello": "world"} +} diff --git a/docs/tests/fixtures/0010_wrong_type_field_int.json b/docs/tests/fixtures/0010_wrong_type_field_int.json new file mode 100644 index 00000000..240b3421 --- /dev/null +++ b/docs/tests/fixtures/0010_wrong_type_field_int.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": 42 +} diff --git a/docs/tests/fixtures/0011_empty_fields.json b/docs/tests/fixtures/0011_empty_fields.json new file mode 100644 index 00000000..2430e390 --- /dev/null +++ b/docs/tests/fixtures/0011_empty_fields.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [] +} diff --git a/docs/tests/fixtures/0012_fields_empty_object.json b/docs/tests/fixtures/0012_fields_empty_object.json new file mode 100644 index 00000000..c18b720a --- /dev/null +++ b/docs/tests/fixtures/0012_fields_empty_object.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + {} + ] +} diff --git a/docs/tests/fixtures/0012_fields_ok.json b/docs/tests/fixtures/0012_fields_ok.json new file mode 100644 index 00000000..39ea4028 --- /dev/null +++ b/docs/tests/fixtures/0012_fields_ok.json @@ -0,0 +1,15 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + { + "id": 1, + "label": "Name", + "slug": "name", + "type_id": "text", + "description": "please put your name here", + "accesses": [] + } + ] +} diff --git a/docs/tests/fixtures/0013_fields_placeholder.json b/docs/tests/fixtures/0013_fields_placeholder.json new file mode 100644 index 00000000..8125d1a2 --- /dev/null +++ b/docs/tests/fixtures/0013_fields_placeholder.json @@ -0,0 +1,16 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + { + "id": 1, + "label": "Name", + "slug": "name", + "type_id": "text", + "description": "please put your name here", + "accesses": [], + "placeholder": "This is my placeholder" + } + ] +} diff --git a/docs/tests/fixtures/0014_fields_multiple.json b/docs/tests/fixtures/0014_fields_multiple.json new file mode 100644 index 00000000..c0b280e0 --- /dev/null +++ b/docs/tests/fixtures/0014_fields_multiple.json @@ -0,0 +1,17 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + { + "id": 1, + "label": "Name", + "slug": "name", + "type_id": "text", + "description": "please put your name here", + "accesses": [], + "placeholder": "This is my placeholder", + "multiple": true + } + ] +} diff --git a/docs/tests/fixtures/0015_fields_items.json b/docs/tests/fixtures/0015_fields_items.json new file mode 100644 index 00000000..b1d6510b --- /dev/null +++ b/docs/tests/fixtures/0015_fields_items.json @@ -0,0 +1,21 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + { + "id": 1, + "label": "Name", + "slug": "name", + "type_id": "text", + "description": "please put your name here", + "accesses": [], + "placeholder": "This is my placeholder", + "multiple": true, + "items": [ + {"label": "first value", "value": "FIRST", "description": "This is my first value"}, + {"label": "second value", "value": "SECOND", "description": "This is the last value"} + ] + } + ] +} diff --git a/docs/tests/fixtures/0016_fields_accesses.json b/docs/tests/fixtures/0016_fields_accesses.json new file mode 100644 index 00000000..ff0a84cc --- /dev/null +++ b/docs/tests/fixtures/0016_fields_accesses.json @@ -0,0 +1,20 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + { + "id": 1, + "label": "Name", + "slug": "name", + "type_id": "text", + "description": "please put your name here", + "accesses": [], + "placeholder": "This is my placeholder", + "accesses": [ + {"access_id": "JEDI", "level": "REQUIRED"}, + {"access_id": "PADAWAN", "level": "EDITABLE"} + ] + } + ] +} diff --git a/docs/tests/fixtures/0017_fields_validations.json b/docs/tests/fixtures/0017_fields_validations.json new file mode 100644 index 00000000..7eca1e09 --- /dev/null +++ b/docs/tests/fixtures/0017_fields_validations.json @@ -0,0 +1,20 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + { + "id": 1, + "label": "Name", + "slug": "name", + "type_id": "text", + "description": "please put your name here", + "accesses": [], + "placeholder": "This is my placeholder", + "validations": [ + {"type": "MINLENGTH", "value": "5", "message": "Your name is too short"}, + {"type": "MAXLENGTH", "value": "100", "message": "Your name is too long"} + ] + } + ] +} diff --git a/docs/tests/fixtures/0018_fields_defaults.json b/docs/tests/fixtures/0018_fields_defaults.json new file mode 100644 index 00000000..97c48af0 --- /dev/null +++ b/docs/tests/fixtures/0018_fields_defaults.json @@ -0,0 +1,17 @@ +{ + "id": 1, + "label": "This is my form title", + "description": "Here's the description of my form.", + "fields": [ + { + "id": 1, + "label": "Name", + "slug": "name", + "type_id": "text", + "description": "please put your name here", + "accesses": [], + "placeholder": "This is my placeholder", + "defaults": ["This is your default"] + } + ] +} diff --git a/docs/tests/fixtures/0020_simple_condition.json b/docs/tests/fixtures/0020_simple_condition.json new file mode 100644 index 00000000..8e8f2fb0 --- /dev/null +++ b/docs/tests/fixtures/0020_simple_condition.json @@ -0,0 +1,60 @@ +{ + "id": 1, + "label": "The Game Form", + "description": "A form to pick a cool game", + "fields": [ + { + "id": 1, + "slug": "do-you-want-to-play-games", + "label": "Do you want to play games?", + "type_id": "checkbox", + "description": "", + "accesses": [] + }, + { + "id": 2, + "slug": "favorite-game", + "label": "Favorite game", + "type_id": "dropdown", + "description": "", + "accesses": [], + "items": [ + { + "value": "BORING", + "label": "Monopoly" + }, + { + "value": "YES", + "label": "Magic Maze" + } + ], + "multiple": false + }, + { + "id": 3, + "slug": "please-explain", + "label": "Please, explain...", + "type_id": "text", + "description": "Please explain why it matters...", + "accesses": [] + } + ], + "conditions": [ + { + "name": "Jeux", + "field_ids": [ + "favorite-game" + ], + "action": "display_iff", + "tests": [ + { + "field_id": "do-you-want-to-play-games", + "operator": "eq", + "values": [ + "true" + ] + } + ] + } + ] +} diff --git a/docs/tests/test_check_schema.py b/docs/tests/test_check_schema.py new file mode 100644 index 00000000..0d4bf57d --- /dev/null +++ b/docs/tests/test_check_schema.py @@ -0,0 +1,7 @@ +from jsonschema import Draft4Validator +from . import schema + + +def test_validate_schema(): + # First, check Schema + assert Draft4Validator.check_schema(schema) is None diff --git a/docs/tests/test_fields.py b/docs/tests/test_fields.py new file mode 100644 index 00000000..492419a4 --- /dev/null +++ b/docs/tests/test_fields.py @@ -0,0 +1,340 @@ +from collections import deque +from copy import deepcopy +from . import validator, _load_fixture + + +def test_wrong_type_fields_int(): + form = _load_fixture('0010_wrong_type_field_int.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + fields_error = errors[0] + assert fields_error.path == deque(['fields']) + assert fields_error.validator == 'type' + assert fields_error.message == "42 is not of type 'array'" + + +def test_wrong_type_fields_dict(): + form = _load_fixture('0010_wrong_type_field_dict.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + fields_error = errors[0] + assert fields_error.path == deque(['fields']) + assert fields_error.validator == 'type' + assert fields_error.message == "{'hello': 'world'} is not of type 'array'" + + +def test_empty_fields(): + form = _load_fixture('0011_empty_fields.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_field_empty_object(): + form = _load_fixture('0012_fields_empty_object.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 6 + for error in errors: + assert error.validator == 'required' + (id_error, slug_error, label_error, type_id_error, + description_error, accesses_error) = errors + assert id_error.message == "'id' is a required property" + assert slug_error.message == "'slug' is a required property" + assert label_error.message == "'label' is a required property" + assert type_id_error.message == "'type_id' is a required property" + assert description_error.message == "'description' is a required property" + assert accesses_error.message == "'accesses' is a required property" + + +def test_field_missing_property(): + form = _load_fixture('0012_fields_ok.json') + for item in ('id', 'slug', 'label', 'type_id', 'description', 'accesses'): + _form = deepcopy(form) + del _form['fields'][0][item] + errors = sorted(validator.iter_errors(_form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'required' + assert error.message == "'{}' is a required property".format(item) +# TODO: Check the field type_id. +# Enum: title, helpText, fieldset, fieldsetTable, separation, checkbox, +# checkboxes, dropdown, radios, radiosButtons, text, paragraph, file, +# date, email, number, + + +def test_fields_ok(): + form = _load_fixture('0012_fields_ok.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_fields_placeholder(): + form = _load_fixture('0013_fields_placeholder.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + form['fields'][0]['placeholder'] = 42 + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "42 is not of type 'string'" + + +def test_fields_multiple(): + form = _load_fixture('0014_fields_multiple.json') + # Should be ok here + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + # Changing the value to another boolean + form['fields'][0]['multiple'] = False + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # Changing the value to a wrong type + form['fields'][0]['multiple'] = "something" + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "'something' is not of type 'boolean'" + + +def test_fields_items(): + form = _load_fixture('0015_fields_items.json') + # Should be ok here + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # Changing the value to an empty list + form['fields'][0]['items'] = [] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # Changing the value to a wrong type + form['fields'][0]['items'] = "something" + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "'something' is not of type 'array'" + + +def _load_15_fields_items(): + form = _load_fixture('0015_fields_items.json') + items = deepcopy(form['fields'][0]['items']) + item = deepcopy(items[0]) + return form, items, item + + +def test_fields_items_validation_no_description(): + form, items, no_description = _load_15_fields_items() + del no_description['description'] + form['fields'][0]['items'] = [no_description] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_fields_items_validation_no_value(): + form, items, no_value = _load_15_fields_items() + del no_value['value'] + form['fields'][0]['items'] = [no_value] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'required' + assert error.message == "'value' is a required property" + + +def test_fields_items_validation_no_label(): + form, items, no_label = _load_15_fields_items() + del no_label['label'] + form['fields'][0]['items'] = [no_label] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'required' + assert error.message == "'label' is a required property" + + +def test_fields_items_validation_wrong_types(): + form, items, item = _load_15_fields_items() + for key in ('label', 'value', 'description'): + wrong_types = deepcopy(item) + wrong_types[key] = 42 + form['fields'][0]['items'] = [wrong_types] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "42 is not of type 'string'" + + +def test_field_accesses(): + form = _load_fixture('0016_fields_accesses.json') + # Should be ok here + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # Changing the value to an empty list + form['fields'][0]['accesses'] = [] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # Changing the value to a wrong type + form['fields'][0]['accesses'] = "something" + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "'something' is not of type 'array'" + + +def _load_16_fields_accesses(): + form = _load_fixture('0016_fields_accesses.json') + items = deepcopy(form['fields'][0]['accesses']) + item = deepcopy(items[0]) + return form, items, item + + +def test_field_accesses_validation_no_access(): + form, items, no_access_id = _load_16_fields_accesses() + del no_access_id['access_id'] + form['fields'][0]['accesses'] = [no_access_id] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'required' + assert error.message == "'access_id' is a required property" + + +def test_field_accesses_validation_no_level(): + form, items, no_level = _load_16_fields_accesses() + del no_level['level'] + form['fields'][0]['accesses'] = [no_level] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'required' + assert error.message == "'level' is a required property" + + +def test_field_accesses_validation_wrong_level(): + form, items, wrong_level = _load_16_fields_accesses() + wrong_level['level'] = 'UNBELIEVABLE' + form['fields'][0]['accesses'] = [wrong_level] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'enum' + levels = ['REQUIRED', 'EDITABLE', 'HIDDEN', 'READONLY'] + assert error.message == f"'UNBELIEVABLE' is not one of {levels}" + + +def test_field_validations(): + form = _load_fixture('0017_fields_validations.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # An empty list should still validate + form['fields'][0]['validations'] = [] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # wrong type should trigger a type error + form['fields'][0]['validations'] = "something" + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "'something' is not of type 'array'" + + +def _load_17_fields_validations(): + form = _load_fixture('0017_fields_validations.json') + items = deepcopy(form['fields'][0]['validations']) + item = deepcopy(items[0]) + return form, items, item + + +def test_field_validations_no_message(): + form, items, no_message = _load_17_fields_validations() + # Removing the overwritten message shouldn't invalidate the field + del no_message['message'] + form['fields'][0]['validations'] = [no_message] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_field_validations_no_type(): + form, items, no_type = _load_17_fields_validations() + del no_type['type'] + form['fields'][0]['validations'] = [no_type] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'required' + assert error.message == "'type' is a required property" + + +def test_field_validations_no_value(): + form, items, no_value = _load_17_fields_validations() + del no_value['value'] + form['fields'][0]['validations'] = [no_value] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'required' + assert error.message == "'value' is a required property" + +# TODO: field validation grids +# 1. don't know if it's possible to validate the grid. +# 2. at least it should be possible to build an enum with available values. +# +# Here they are: +# * text: MINLENGTH, MAXLENGTH, REGEXP +# * paragraph: MINLENGTH, MAXLENGTH, REGEXP +# * date: GT, GTE, LT, LTE, EQ, NEQ, IS_AGE_ABOVE (>=), IS_AGE_UNDER (<), +# IS_DATE_IN_THE_PAST, IS_DATE_IN_THE_FUTURE +# * number: GT, GTE, LT, LTE, EQ, NEQ + + +def _load_18_fields_defaults(): + form = _load_fixture('0018_fields_defaults.json') + field = form['fields'][0] + return form, field + + +def test_fields_defaults(): + form, field = _load_18_fields_defaults() + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_fields_defaults_empty(): + form, field = _load_18_fields_defaults() + # Empty arrays should work + field['defaults'] = [] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_fields_defaults_wrong_type(): + form, field = _load_18_fields_defaults() + # Wrong type in array + for value in (42, True, None): + field['defaults'] = [value] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == f"{value} is not of type 'string'" + + +def test_fields_defaults_bad_type(): + form, field = _load_18_fields_defaults() + # Plain wrong type + field['defaults'] = "something" + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "'something' is not of type 'array'" diff --git a/docs/tests/test_forms.py b/docs/tests/test_forms.py new file mode 100644 index 00000000..c6b5aa35 --- /dev/null +++ b/docs/tests/test_forms.py @@ -0,0 +1,52 @@ +from . import validator, _load_fixture + + +def test_empty_form(): + form = _load_fixture('0000_empty.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 3 + # All of these errors are of type 'required' + for error in errors: + assert error.validator == 'required' + id_error, label_error, description_error = errors + assert id_error.message == "'id' is a required property" + assert label_error.message == "'label' is a required property" + assert description_error.message == "'description' is a required property" + + +def test_just_id(): + form = _load_fixture('0001_just_id.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 2 + # All of these errors are of type 'required' + for error in errors: + assert error.validator == 'required' + label_error, description_error = errors + assert label_error.message == "'label' is a required property" + assert description_error.message == "'description' is a required property" + + +def test_id_and_label(): + form = _load_fixture('0002_id_label.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + # All of these errors are of type 'required' + for error in errors: + assert error.validator == 'required' + assert len(errors) == 1 + description_error = errors[0] + assert description_error.message == "'description' is a required property" + + +def test_id_not_integer(): + form = _load_fixture('0003_id_not_integer.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + id_error = errors[0] + assert id_error.validator == "type" + assert id_error.message == "'hello' is not of type 'integer'" + + +def test_id_and_label_and_description(): + form = _load_fixture('0004_id_label_description.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 diff --git a/docs/tests/test_forms_conditions.py b/docs/tests/test_forms_conditions.py new file mode 100644 index 00000000..00ceab0b --- /dev/null +++ b/docs/tests/test_forms_conditions.py @@ -0,0 +1,153 @@ +from copy import deepcopy + +from . import validator, _load_fixture + + +def test_simple_condition(): + form = _load_fixture('0020_simple_condition.json') + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # An empty list should still validate + form['conditions'] = [] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + # wrong type should trigger a type error + form['conditions'] = "something" + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "'something' is not of type 'array'" + + +def _load_20_simple_condition(): + form = _load_fixture('0020_simple_condition.json') + condition = deepcopy(form['conditions'][0]) + return form, condition + + +def test_simple_condition_validation_no_name(): + form, no_name = _load_20_simple_condition() + # FIXME: this should fail validation ; name should be mandatory. + del no_name['name'] + form['conditions'] = [no_name] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_simple_condition_validation_no_fields(): + form, no_fields = _load_20_simple_condition() + # FIXME: this should fail validation ; field IDs should be mandatory. + del no_fields['field_ids'] + form['conditions'] = [no_fields] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_simple_condition_validation_fields_empty(): + form, fields_empty = _load_20_simple_condition() + # FIXME: this should fail validation ; field IDs should not be empty. + fields_empty['field_ids'] = [] + form['conditions'] = [fields_empty] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_simple_condition_validation_no_tests(): + form, no_tests = _load_20_simple_condition() + # FIXME: this should fail validation ; tests should be mandatory. + del no_tests['tests'] + form['conditions'] = [no_tests] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_simple_condition_validation_empty_tests(): + form, empty_tests = _load_20_simple_condition() + # FIXME: this should fail validation ; tests should not be empty. + empty_tests['tests'] = [] + form['conditions'] = [empty_tests] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_simple_condition_validation_no_action(): + form, no_action = _load_20_simple_condition() + # FIXME: this should fail validation ; it should have an action property. + del no_action['action'] + form['conditions'] = [no_action] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def _load_20_simple_condition_test(): + form = _load_fixture('0020_simple_condition.json') + condition = deepcopy(form['conditions'][0]) + condition_test = deepcopy(condition['tests'][0]) + return form, condition, condition_test + + +def test_simple_condition_test_wrong_type(): + form, condition, condition_test = _load_20_simple_condition_test() + + # wrong type for test + wrong_type_condition = deepcopy(condition) + wrong_type_condition['tests'] = "something" + form['conditions'] = [wrong_type_condition] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'type' + assert error.message == "'something' is not of type 'array'" + + +def test_simple_condition_test_no_field_id(): + form, condition, no_field_id = _load_20_simple_condition_test() + # FIXME: this should fail validation ; it should have a field_id property. + # Remove field_id + del no_field_id['field_id'] + no_field_id_condition = deepcopy(condition) + no_field_id_condition['tests'] = [no_field_id] + form['conditions'] = [no_field_id_condition] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_simple_condition_test_no_op(): + form, condition, no_op = _load_20_simple_condition_test() + # FIXME: this should fail validation ; it should have an operator. + # Remove operator + del no_op['operator'] + no_op_condition = deepcopy(condition) + no_op_condition['tests'] = [no_op] + form['conditions'] = [no_op_condition] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 + + +def test_simple_condition_test_bad_op(): + form, condition, bad_op = _load_20_simple_condition_test() + # Change operator to a wrong value + bad_op['operator'] = "meh" + bad_op_condition = deepcopy(condition) + bad_op_condition['tests'] = [bad_op] + form['conditions'] = [bad_op_condition] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 1 + error = errors[0] + assert error.validator == 'enum' + assert error.message == "'meh' is not one of ['eq']" + + +def test_simple_condition_test_no_values(): + form, condition, no_values = _load_20_simple_condition_test() + # FIXME: this should fail validation ; it should have a value. + # Remove values + del no_values['values'] + no_values_condition = deepcopy(condition) + no_values_condition['tests'] = [no_values] + form['conditions'] = [no_values_condition] + errors = sorted(validator.iter_errors(form), key=lambda e: e.path) + assert len(errors) == 0 diff --git a/tox.ini b/tox.ini index 32d79280..4ce87b95 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = django18-py{27,35} django19-py{27,35} django110-py{27,35} + spectest flake8 isort-check skipsdist = True @@ -55,6 +56,18 @@ deps = isort commands = isort --recursive formidable +[testenv:spectest] +basepython = python3.6 +usedevelop = False +skip_install = True +changedir = docs +deps = + pytest + jsonschema + pyyaml +commands = + pytest -s + ; Not included in the test env run with `tox` [testenv:docs] deps = From e8f62957754375b6afa626c72dba9dea55a6dc17 Mon Sep 17 00:00:00 2001 From: Bruno Bord Date: Tue, 13 Feb 2018 15:37:14 +0100 Subject: [PATCH 2/2] unrelated: force Python3.6 for the `flake8` job valid python 2.7 code is probably safe flake8-wise. It avoids hitting "syntax errors" when checking pure python3.6 test code. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 4ce87b95..80dbbbe5 100644 --- a/tox.ini +++ b/tox.ini @@ -27,6 +27,7 @@ commands = python manage.py test {posargs} [testenv:flake8] +basepython = python3.6 usedevelop = False skip_install = True changedir = {toxinidir}