From 76ef4a7e109f166d993fb0f269bcf921cf72f6d7 Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Mon, 25 Mar 2024 16:37:08 +0000 Subject: [PATCH] Test custom validators support and improve configuration of them --- setup.cfg | 3 + tests/test_validation.py | 119 ++++++++++++++++++++++++++ tg/controllers/decoratedcontroller.py | 2 +- tg/validation.py | 18 ++-- 4 files changed, 130 insertions(+), 12 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7c4c30c0..e1a8e189 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [aliases] # A handy alias to make a release to pypi release = egg_info -RDb "" sdist bdist_egg register upload + +[tool:pytest] +addopts = --cov=tg --cov-report lcov --cov-report term --cov-append diff --git a/tests/test_validation.py b/tests/test_validation.py index 99b9eb56..fe6f357c 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -10,6 +10,8 @@ import tests import tg +from tg.configuration.utils import TGConfigError +from tg.configurator.fullstack import FullStackApplicationConfigurator from tg.controllers import TGController, DecoratedController, abort from tg.controllers.util import validation_errors_response from tg.decorators import expose, validate, before_render, before_call, Decoration @@ -21,6 +23,8 @@ from tg.validation import TGValidationError, _ValidationStatus, Convert, RequireValue from tg.i18n import lazy_ugettext as l_ +from webtest import TestApp + def setup_module(): setup_session_dir() @@ -551,3 +555,118 @@ def test_double_chain_validation(self): def test_last_chain_validation(self): res = self.app.get('/chain_validation_begin', params={'val': 0}, status=412) assert res.json == json.loads('{"errors":{"val":"Invalid"},"values":{"val":"0"}}') + + +class TestValidationConfiguration: + def create_app(self, root_controller, options=None): + cfg = FullStackApplicationConfigurator() + cfg.update_blueprint({'root_controller': root_controller}) + cfg.update_blueprint(options or {}) + + app = TestApp(cfg.make_wsgi_app({ + 'debug': False, + 'errorpage.enabled': False, + 'trace_errors.enable': False + }, {})) + return app + + def test_no_validation_function(self): + class FakeSchema: + pass + + class RootController(TGController): + @validate(FakeSchema()) + @expose("text/plain") + def test(self, **kwargs): + return "HI" + + app = self.create_app(RootController()) + + with pytest.raises(TGConfigError) as exc_info: + app.get("/test", {"value": 5}) + assert "No validation validator function found for" in str(exc_info.value) + assert "FakeSchema" in str(exc_info.value) + + def test_custom_validation(self): + class FakeSchema: + pass + + def validate_fake_schema(schema, params): + if params.get("fail"): + raise TGValidationError("Invalid params", value=params, + error_dict={"fail": "Fail is true"}) + return params + + class RootController(TGController): + @validate(FakeSchema()) + @expose("text/plain") + def test(self, **kwargs): + if tg.request.validation.errors: + return str(tg.request.validation.errors) + return "HI" + + app = self.create_app(RootController(), { + "validation.validators": {FakeSchema: validate_fake_schema} + }) + + resp = app.get("/test", {"value": 5}) + assert resp.text == "HI" + + resp = app.get("/test", {"fail": 1}) + assert "Fail is true" in resp.text + + def test_custom_validation_error(self): + class FakeSchema: + pass + + def validate_fake_schema(schema, params): + if params.get("fail"): + raise FakeError() + return params + + class FakeError(Exception): + pass + + def explode_fake_error(error): + return {"values": {"fail": True}, "errors": "fail was true"} + + class RootController(TGController): + @validate(FakeSchema()) + @expose("text/plain") + def test(self, **kwargs): + if tg.request.validation.errors: + return str(tg.request.validation.errors) + return "HI" + + app = self.create_app(RootController(), { + "validation.validators": {FakeSchema: validate_fake_schema}, + "validation.exceptions": [FakeError], + "validation.explode": {FakeError: explode_fake_error} + }) + + resp = app.get("/test", {"value": 5}) + assert resp.text == "HI" + + resp = app.get("/test", {"fail": 1}) + assert "fail was true" in resp.text + + # Do not provide explode function + app = self.create_app(RootController(), { + "validation.validators": {FakeSchema: validate_fake_schema}, + "validation.exceptions": [FakeError] + }) + with pytest.raises(TGConfigError) as exc_info: + resp = app.get("/test", {"fail": 1}) + assert "No validation explode function found for" in str(exc_info.value) + assert "FakeError" in str(exc_info.value) + + # Do not provide missing explode function + app = self.create_app(RootController(), { + "validation.validators": {FakeSchema: validate_fake_schema}, + "validation.exceptions": [FakeError], + "validation.explode": {FakeError: None} + }) + with pytest.raises(TGConfigError) as exc_info: + resp = app.get("/test", {"fail": 1}) + assert "No validation explode function found for" in str(exc_info.value) + assert "FakeError" in str(exc_info.value) diff --git a/tg/controllers/decoratedcontroller.py b/tg/controllers/decoratedcontroller.py index bc59141c..70eea8b8 100644 --- a/tg/controllers/decoratedcontroller.py +++ b/tg/controllers/decoratedcontroller.py @@ -272,7 +272,7 @@ def _process_validation_errors(cls, controller, remainder, params, exception, co validation_status.errors = exploded_validation['errors'] validation_status.values = exploded_validation['values'] else: - raise TGConfigError(f"No validation explode function found for: {exception}") + raise TGConfigError(f"No validation explode function found for: {exception.__class__}") # Get the error handler associated to the current validation status. error_handler = validation_status.error_handler diff --git a/tg/validation.py b/tg/validation.py index d22eafd6..7b908214 100644 --- a/tg/validation.py +++ b/tg/validation.py @@ -81,8 +81,13 @@ def check(self, config, method, params): raise TGValidationError(TGValidationError.make_compound_message(errors), value=params, error_dict=errors) - - elif isinstance(validators, tuple(validation_validators.keys())): + elif hasattr(validators, 'validate') and getattr(self, 'needs_controller', False): + # An object with a "validate" method - call it with the parameters + validated_params = validators.validate(method, params) + elif hasattr(validators, 'validate'): + # An object with a "validate" method - call it with the parameters + validated_params = validators.validate(params) + else: schema_class = validators.__class__ validation_function = None for supported_class in validation_validators: @@ -94,15 +99,6 @@ def check(self, config, method, params): validated_params = validation_function(validators, params) - elif hasattr(validators, 'validate') and getattr(self, 'needs_controller', False): - # An object with a "validate" method - call it with the parameters - validated_params = validators.validate(method, params) - - elif hasattr(validators, 'validate'): - # An object with a "validate" method - call it with the parameters - validated_params = validators.validate(params) - - return validated_params