"""Validator Tests. Testing generic validator methods """ import json from unittest.mock import MagicMock from unittest.mock import patch from flask import Request from flask import Response from jsonschema import Draft3Validator from oto import response import pytest from werkzeug import datastructures from product_digital.constants import header from product_digital.validation import json_schema from product_digital.validation.schema import json_validators @pytest.fixture def schema(): """Test schema to validate against. Returns: dict: validation schema """ return { 'type': 'object', 'properties': { 'string-property': {'type': 'string'}, 'numeric-property': {'type': 'number'} } } @pytest.fixture def validator(schema): """Validator instance that uses our test schema. Returns: Draft3Validator: validator instance """ return Draft3Validator(schema) @pytest.fixture def valid_data(): """Data that will pass validation against the test schema. Returns: dict: valid data """ return { 'string-property': 'this is a string', 'numeric-property': 123 } @pytest.fixture def invalid_data(): """Data that will fail validation against the test schema. Returns: dict: invalid data """ return { 'string-property': 79.3, 'numeric-property': 'i am a number, i swear' } @pytest.fixture def invalid_data_response(): """The formatted validation response for the invalid data. Returns: dict: invalid data response """ return { 'string-property': { 'validator_value': 'string', 'validator': 'type', 'message': "79.3 is not of type 'string'", 'error_code': 'string-property_type', }, 'numeric-property': { 'validator_value': 'number', 'validator': 'type', 'message': "'i am a number, i swear' is not of type 'number'", 'error_code': 'numeric-property_type', } } @pytest.fixture def teapot_status(): """Non-success status code to use when testing validation failure. Returns: int: status code """ return 418 @pytest.fixture def mock_request_with_args(mocker, resource): """Fixture for request with args.""" args_dict = {'foo': 'bar', 'baz': 'boink'} return mocker.Mock( args=mocker.Mock(to_dict=mocker.Mock(return_value=args_dict)), url_rule=mocker.Mock(rule=resource), method='GET') @pytest.fixture def mock_request_with_body(mocker, resource): """Fixture for request with body.""" get_json = mocker.Mock(return_value={'int_prop': 1, 'string_prop': 'hi'}) return mocker.Mock( get_json=get_json, url_rule=mocker.Mock(rule=resource), method='GET') @pytest.fixture def mock_request_with_headers(mocker, resource): """Fixture for request with headers.""" headers = datastructures.EnvironHeaders({'HTTP_CORRELATION_ID': 'bar'}) return mocker.Mock( headers=headers, url_rule=mocker.Mock(rule=resource), method='GET') @pytest.fixture def resource(): """Return a test resource name.""" return '/test_route' def test_validate_success(valid_data, validator): """Test status and message when data is valid.""" response = json_schema.validate(valid_data, validator) assert response.status == 200 assert response.message == {'status': 'ok'} def test_validate_failure( invalid_data, invalid_data_response, validator, teapot_status): """Test status and errors when data is invalid.""" response = json_schema.validate(invalid_data, validator, teapot_status) message = response.errors.get('message') assert response.status == teapot_status assert message == invalid_data_response def test_validate_request_args(mocker, mock_request_with_args, resource): """Test to validate request args. Test that a query args validator is constructed and called with the args from the given request. """ validator = mocker.Mock() mocker.patch.object( json_validators, 'query_args_validator', return_value=validator) mocker.patch.object(json_schema, 'validate', return_value=response.Response()) @json_schema.validate_request_args def test_func(): pass with patch.object(json_schema, 'request', mock_request_with_args): test_func() json_validators.query_args_validator.assert_called_with() json_schema.validate.assert_called_with( mock_request_with_args.args.to_dict(), validator) def test_validate_headers(mocker, mock_request_with_headers, resource): """Test to validate headers. Test that a headers validator is constructed and called with the headers from the given request. """ validator = mocker.Mock() mocker.patch.object( json_validators, 'headers_validator', return_value=validator) mocker.patch.object(json_schema, 'validate', return_value=response.Response()) @json_schema.validate_request_headers def test_func(): pass with patch.object(json_schema, 'request', mock_request_with_headers): test_func() json_validators.headers_validator.assert_called_with() json_schema.validate.assert_called_with( dict(mock_request_with_headers.headers), validator) def test_validate_body(mocker, mock_request_with_body, resource): """Test to validate body. Test that a body validator is constructed and called with the body from the given request. """ validator = mocker.Mock() mocker.patch.object( json_validators, 'body_validator', return_value=validator) mocker.patch.object(json_schema, 'validate', return_value=response.Response()) @json_schema.validate_request_body def test_func(): pass with patch.object(json_schema, 'request', mock_request_with_body): test_func() json_validators.body_validator.assert_called_with() json_schema.validate.assert_called_with( mock_request_with_body.get_json(), validator) def test_wrap_request_validation_success(mocker, resource): """Test that on validation success the wrapped function is called.""" request = mocker.Mock() validation_function = mocker.Mock(return_value=response.Response()) @json_schema._wrap_request_validation(validation_function) def wrapped_function(): return 'i fired!' with patch.object(json_schema, 'request', request): assert wrapped_function() == 'i fired!' def test_wrap_request_validation_failure(mocker): """Test validation failure. Test that on validation failure an error is returned and the wrapped function never gets called. """ request = mocker.Mock() errors = {'message': 'something bad happened here'} validation_function = mocker.Mock( return_value=response.Response(status=400, errors=errors)) call_me_maybe = mocker.Mock() @json_schema._wrap_request_validation(validation_function) def wrapped_function(ignored_function): ignored_function() with patch.object(json_schema, 'request', request): result = wrapped_function(call_me_maybe) result_json = json.loads(result.data.decode('utf-8')) assert result.status_code == 400 assert result_json == errors call_me_maybe.assert_not_called() def run_with_reject_grass_headers(headers): """Submit request with headers and get response.""" request = MagicMock(spec=Request, headers=headers) success_response = MagicMock(spec=Response, status_code=200) @json_schema.reject_grass_headers def inner(): return success_response with patch.object(json_schema, 'request', request): return inner() @pytest.mark.parametrize('grass_headers', [ {header.GRASS_ACCOUNT_ID: 1234, header.GRASS_ACCOUNT_TYPE: 'subaccount'}, {header.GRASS_ACCOUNT_ID: 1234}, {header.GRASS_ACCOUNT_TYPE: 'subaccount'}, ]) def test_reject_grass_headers(grass_headers): """Test that 400 return when grass request made.""" result = run_with_reject_grass_headers(grass_headers) assert result.status_code == 400 def test_reject_grass_headers_passing(): """Test that inner function called when not grass.""" result = run_with_reject_grass_headers({ 'foo': 'bar'}) assert result.status_code == 200