From c12681bd5c126d5c5a3c5ea542946908d4ac927a Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Mon, 7 Dec 2020 16:33:54 -0600 Subject: [PATCH] Move validation from pgwui_server to pgwui_common --- README.rst | 4 + src/pgwui_common/check_settings.py | 238 ++++++++++++ src/pgwui_common/constants.py | 27 ++ src/pgwui_common/exceptions.py | 17 + tests/test_check_settings.py | 581 +++++++++++++++++++++++++++++ 5 files changed, 867 insertions(+) create mode 100644 src/pgwui_common/check_settings.py create mode 100644 src/pgwui_common/constants.py create mode 100644 tests/test_check_settings.py diff --git a/README.rst b/README.rst index eff18f6..cc52ba4 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,10 @@ PGWUI_Common provides: * Code used to establish `routes`_, called by PGWUI_Server or whatever else is used to configure `Pyramid`_. + * Functionality which validates all installed PGWUI component + settings. Validation happens when the PGWUI_Common component + is configured. + The official PGWUI components based on PGWUI_Common are highly configurable. The web page templates used to generate HTML files, the CSS files, the static HTML files, and the location of the web pages diff --git a/src/pgwui_common/check_settings.py b/src/pgwui_common/check_settings.py new file mode 100644 index 0000000..1cdc1e2 --- /dev/null +++ b/src/pgwui_common/check_settings.py @@ -0,0 +1,238 @@ +# Copyright (C) 2018, 2019, 2020 The Meme Factory, Inc. +# http://www.karlpinc.com/ + +# This file is part of PGWUI_Common. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +'''Validate PGWUI_Core and PGWUI_Common configuration +''' + +import re +from ast import literal_eval + +from . import constants +from . import exceptions as ex +from . import checkset + + +# Regular expressions for page "source" values, by type +URL_RE = re.compile('^(?:(?:[^:/]+:)?//[^/])|(?:/(?:[^/]|$))') + + +def key_to_ini(key): + '''Convert the setting key to a key used in an ini file's declaration + ''' + return 'pgwui:{}'.format(key) + + +def require_setting(errors, setting, pgwui_settings, formatter): + if setting not in pgwui_settings: + errors.append(ex.MissingSettingError(formatter(setting))) + return False + return True + + +def boolean_setting(errors, setting, pgwui_settings): + if setting in pgwui_settings: + try: + val = literal_eval(pgwui_settings[setting]) + except ValueError: + val = None + if (val is not True + and val is not False): + errors.append(ex.NotBooleanSettingError( + key_to_ini(setting), pgwui_settings[setting])) + + +def validate_setting_values(errors, settings): + '''Check each settings value for validity + ''' + pgwui_settings = settings['pgwui'] + + # pg_host can be missing, it defaults to the Unix socket (in psycopg2) + + # pg_port can be missing, it defaults to 5432 (in psycopg2) + + # default_db can be missing, then the user sees no default + + # dry_run + require_setting(errors, 'dry_run', pgwui_settings, key_to_ini) + boolean_setting(errors, 'dry_run', pgwui_settings) + + # route_prefix can be missing, defaults to no route prefix which is fine. + + # routes can be missing, sensible defaults are built-in. + + # validate_hmac + boolean_setting(errors, 'validate_hmac', pgwui_settings) + + +def do_validate_hmac(settings): + '''True unless the user has specificly rejected hmac validation + ''' + pgwui_settings = settings['pgwui'] + return ('validate_hmac' not in pgwui_settings + or literal_eval(pgwui_settings['validate_hmac'])) + + +def validate_hmac(errors, settings): + '''Unless otherwise requested, validate the session.secret setting''' + if not do_validate_hmac(settings): + return + + if 'session.secret' not in settings: + errors.append(ex.NoHMACError()) + return + + if len(settings['session.secret']) != constants.HMAC_LEN: + errors.append(ex.HMACLengthError()) + return + + +def page_key_to_ini(page_key, subkey): + '''Convert the page setting subkey to a ini file declaration + ''' + return key_to_ini(f'{page_key}:{subkey}') + + +def require_page_settings(errors, required_settings, page_settings, page_key): + '''Check for required keys in the page setting + ''' + def subkey_to_ini(subkey): + return page_key_to_ini(page_key, subkey) + + have_settings = True + for subkey in required_settings: + have_settings &= require_setting( + errors, subkey, page_settings, subkey_to_ini) + + return have_settings + + +def validate_url_source(errors, page_key, source): + '''Validate the page setting "source" for URLs + ''' + if URL_RE.match(source): + return + errors.append(ex.BadURLSourceError( + page_key_to_ini(page_key, 'source'), source)) + + +def validate_url_path(errors, page_key, page_settings): + '''Validate the page setting "url_path" + ''' + url_path = page_settings['url_path'] + if url_path[0:1] == '/': + return + errors.append(ex.BadFileURLPathError( + page_key_to_ini(page_key, 'url_path'), url_path)) + + +def validate_file_source(errors, page_key, source): + '''Validate the page setting "source" for files + ''' + if source[0:1] == '/': + return + errors.append(ex.BadFileSourceError( + page_key_to_ini(page_key, 'file'), source)) + + +def validate_route_source(errors, page_key, source): + '''Validate the page setting "source" for routes + + The routes are not yet established, so we don't confirm + existance at this point. + ''' + if source != '': + return + errors.append(ex.BadRouteSourceError( + page_key_to_ini(page_key, 'route'), source)) + + +def validate_asset_source(errors, page_key, source): + '''Validate the page setting "source" for assets + ''' + if source != '': + return + errors.append(ex.BadAssetSourceError( + page_key_to_ini(page_key, 'asset'), source)) + + +def validate_file_content(errors, page_key, page_settings, source): + '''Validate the content of a "file" page setting + ''' + validate_file_source(errors, page_key, source) + if require_page_settings( + errors, ['url_path'], page_settings, page_key): + validate_url_path(errors, page_key, page_settings) + errors.extend(checkset.unknown_settings( + f'pgwui:{page_key}', ['type', 'source', 'url_path'], page_settings)) + + +def validate_type_content(errors, page_key, page_settings): + '''Validate the page setting's "type", and other page setting content + based on the type + ''' + type = page_settings['type'] + source = page_settings['source'] + if type == 'URL': + validate_url_source(errors, page_key, source) + errors.extend(checkset.unknown_settings( + 'pgwui_common', ['type', 'source'], page_settings)) + return + if type == 'file': + validate_file_content(errors, page_key, page_settings, source) + return + if type == 'route': + validate_route_source(errors, page_key, source) + errors.extend(checkset.unknown_settings( + 'pgwui_common', ['type', 'source'], page_settings)) + return + if type == 'asset': + validate_asset_source(errors, page_key, source) + errors.extend(checkset.unknown_settings( + 'pgwui_common', ['type', 'source'], page_settings)) + return + + errors.append(ex.BadPageTypeError( + page_key_to_ini(page_key, 'type'), type)) + + +def validate_page_setting(errors, settings, page_key): + '''Validate the multiple values of the page setting + ''' + pgwui_settings = settings['pgwui'] + if page_key not in pgwui_settings: + return + + page_settings = pgwui_settings[page_key] + if not require_page_settings( + errors, ['type', 'source'], page_settings, page_key): + return + + validate_type_content(errors, page_key, page_settings) + + +def validate_settings(errors, settings): + '''Validate all core settings + ''' + validate_setting_values(errors, settings) + validate_hmac(errors, settings) + validate_page_setting(errors, settings, 'home_page') + validate_page_setting(errors, settings, 'menu_page') diff --git a/src/pgwui_common/constants.py b/src/pgwui_common/constants.py new file mode 100644 index 0000000..100fe56 --- /dev/null +++ b/src/pgwui_common/constants.py @@ -0,0 +1,27 @@ +# Copyright (C) 2020 The Meme Factory, Inc. http://www.karlpinc.com/ + +# This file is part of PGWUI_Common. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +'''Constants shared by multiple modules +''' + + +# Required length of HMAC value +HMAC_LEN = 40 diff --git a/src/pgwui_common/exceptions.py b/src/pgwui_common/exceptions.py index b91e9c9..3ab6784 100644 --- a/src/pgwui_common/exceptions.py +++ b/src/pgwui_common/exceptions.py @@ -24,6 +24,7 @@ ''' from pgwui_core import exceptions as core_ex +from . import constants class Error(core_ex.PGWUIError): @@ -45,6 +46,22 @@ class MenuPageInRoutes(Info): 'and the pgwui.menu_page setting used instead') +class BadHMACError(Error): + pass + + +class NoHMACError(BadHMACError): + def __init__(self): + super().__init__('Missing session.secret configuration') + + +class HMACLengthError(BadHMACError): + def __init__(self): + super().__init__( + 'The session.secret value is not {} characters in length' + .format(constants.HMAC_LEN)) + + class UnknownSettingKeyError(Error): def __init__(self, key): super().__init__('Unknown PGWUI setting: {}'.format(key)) diff --git a/tests/test_check_settings.py b/tests/test_check_settings.py new file mode 100644 index 0000000..9e08140 --- /dev/null +++ b/tests/test_check_settings.py @@ -0,0 +1,581 @@ +# Copyright (C) 2018, 2019, 2020 The Meme Factory, Inc. +# http://www.karlpinc.com/ + +# This file is part of PGWUI_Common. +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see +# . +# + +# Karl O. Pinc + +import pytest + +import pgwui_common.exceptions as ex +import pgwui_common +from pgwui_common import check_settings +from pgwui_common import constants +from pgwui_testing import testing + +# Mark all tests as unit tests +pytestmark = pytest.mark.unittest + +mock_unknown_settings = testing.make_mock_fixture( + pgwui_common.checkset, 'unknown_settings') + + +# key_to_ini() + +def test_key_to_ini(): + '''The return value is as expected + ''' + key = 'pgwui_example' + result = check_settings.key_to_ini(key) + + assert result == 'pgwui:' + key + + +mock_key_to_ini = testing.make_mock_fixture( + check_settings, 'key_to_ini') + + +# require_setting() + +def test_require_setting_missing(): + '''Deliver exception when a required setting is missing''' + errors = [] + check_settings.require_setting(errors, 'key', {}, lambda x: x) + + assert errors + assert isinstance(errors[0], ex.MissingSettingError) + + +def test_require_setting_present(): + '''Does nothing when a required setting is present''' + errors = [] + check_settings.require_setting( + errors, 'key', {'key': 'value'}, lambda x: x) + + assert errors == [] + + +mock_require_setting = testing.make_mock_fixture( + check_settings, 'require_setting') + + +# boolean_setting() + +def test_boolean_setting_missing(): + '''Does nothing when the setting is not in the settings''' + errors = [] + check_settings.boolean_setting(errors, 'key', {}) + + assert errors == [] + + +def test_boolean_setting_true(): + '''Does nothing when the setting is "True"''' + errors = [] + check_settings.boolean_setting(errors, 'key', {'key': 'True'}) + + assert errors == [] + + +def test_boolean_setting_false(): + '''Does nothing when the setting is "False"''' + errors = [] + check_settings.boolean_setting(errors, 'key', {'key': 'False'}) + + assert errors == [] + + +def test_boolean_setting_notboolean(): + '''Deliver an exception when the setting does not evaluate to a boolean''' + errors = [] + check_settings.boolean_setting(errors, 'key', {'key': '0'}) + + assert errors + assert isinstance(errors[0], ex.NotBooleanSettingError) + + +def test_boolean_setting_notparsable(): + '''Deliver an exception when the setting does not evaluate to a + boolean because it is not parseable + ''' + errors = [] + check_settings.boolean_setting(errors, 'key', {'key': 'a'}) + + assert errors + assert isinstance(errors[0], ex.NotBooleanSettingError) + + +mock_boolean_setting = testing.make_mock_fixture( + check_settings, 'boolean_setting') + + +# validate_setting_values() + +def test_validate_setting_values(mock_require_setting, mock_boolean_setting): + '''Calls require_setting() and boolean_setting()''' + + check_settings.validate_setting_values([], {'pgwui': {}}) + + assert mock_require_setting.called + assert mock_boolean_setting.called + + +mock_validate_setting_values = testing.make_mock_fixture( + check_settings, 'validate_setting_values') + + +# do_validate_hmac() + +def test_do_validate_hmac_none(): + '''pgwui.validate_hmac defaults to True''' + assert check_settings.do_validate_hmac({'pgwui': {}}) is True + + +def test_do_validate_hmac_True(): + '''Require hmac validation when pgwui.validate_hmac is True''' + result = check_settings.do_validate_hmac( + {'pgwui': {'validate_hmac': 'True'}}) + assert result is True + + +def test_do_validate_hmac_False(): + '''No hmac validation when pgwui.validate_hmac is False''' + result = check_settings.do_validate_hmac( + {'pgwui': {'validate_hmac': 'False'}}) + assert result is False + + +mock_do_validate_hmac = testing.make_mock_fixture( + check_settings, 'do_validate_hmac') + + +# validate_hmac() + +def test_validate_hmac_unvalidated(mock_do_validate_hmac): + '''No error is returned when hmac validation is off''' + mock_do_validate_hmac.return_value = False + errors = [] + check_settings.validate_hmac(errors, {}) + + assert errors == [] + + +def test_validate_hmac_success(mock_do_validate_hmac): + '''No error is returned when hmac is validated an the right length''' + mock_do_validate_hmac.return_value = True + errors = [] + check_settings.validate_hmac( + errors, {'session.secret': 'x' * constants.HMAC_LEN}) + + assert errors == [] + + +def test_validate_hmac_missing(mock_do_validate_hmac): + '''Deliver error when hmac is validated and missing''' + mock_do_validate_hmac.return_value = True + errors = [] + check_settings.validate_hmac(errors, {}) + + assert errors + assert isinstance(errors[0], ex.NoHMACError) + + +def test_validate_hmac_length(mock_do_validate_hmac): + '''Deliver error when hmac is validated and the wrong length''' + mock_do_validate_hmac.return_value = True + errors = [] + check_settings.validate_hmac(errors, {'session.secret': ''}) + + assert errors + assert isinstance(errors[0], ex.HMACLengthError) + + +mock_validate_hmac = testing.make_mock_fixture( + check_settings, 'validate_hmac') + + +# page_key_to_ini() + +def test_page_key_to_ini(mock_key_to_ini): + '''key_to_ini() is called, expected result returned + ''' + mock_key_to_ini.return_value = 'foo' + result = check_settings.page_key_to_ini(None, None) + assert result == 'foo' + + +mock_page_key_to_ini = testing.make_mock_fixture( + check_settings, 'page_key_to_ini') + + +# require_page_settings() + +@pytest.mark.parametrize( + ('required_settings', 'rs_results', 'expected'), [ + # Settings exist, return True + (['s1', 's2'], [True, True], True), + # One setting does not exist, return False + (['s1', 's2'], [True, False], False)]) +def test_require_page_settings_result( + mock_page_key_to_ini, mock_require_setting, + required_settings, rs_results, expected): + '''Returns the expected result + ''' + mock_require_setting.side_effect = rs_results + result = check_settings.require_page_settings( + None, required_settings, None, None) + assert result == expected + + +def test_require_page_settings_subfunc( + mock_page_key_to_ini, mock_require_setting): + '''Calls page_key_to_ini() when function is passed to require_setting() + ''' + def mock_rs(x, subkey, z, subkey_to_ini): + subkey_to_ini(subkey) + return True + + required_settings = ['s1', 's2'] + mock_require_setting.side_effect = mock_rs + check_settings.require_page_settings(None, required_settings, None, None) + + assert mock_page_key_to_ini.call_count == len(required_settings) + + +mock_require_page_settings = testing.make_mock_fixture( + check_settings, 'require_page_settings') + + +# validate_url_source() + +@pytest.mark.parametrize( + ('source', 'expected_error'), [ + ('/', None), + ('/foo', None), + ('//www.example.com', None), + ('//www.example.com/', None), + ('//www.example.com/foo', None), + ('http://www.example.com', None), + ('https://www.example.com', None), + ('anything://www.example.com', None), + ('http://www.example.com/', None), + ('http://www.example.com/foo', None), + # No domain + ('//', ex.BadURLSourceError), + # Nothing + ('', ex.BadURLSourceError), + # Missing / after scheme + ('http:/www.example.com', ex.BadURLSourceError), + # Extra / after scheme + ('http:///www.example.com', ex.BadURLSourceError)]) +def test_validate_url_source(mock_page_key_to_ini, source, expected_error): + '''The test url produces the expected error, or no error as may be + ''' + errors = [] + check_settings.validate_url_source(errors, None, source) + + if expected_error: + assert len(errors) == 1 + assert isinstance(errors[0], expected_error) + else: + assert len(errors) == 0 + + +mock_validate_url_source = testing.make_mock_fixture( + check_settings, 'validate_url_source') + + +# validate_url_path() + +@pytest.mark.parametrize( + ('path',), [ + ('',), + ('foo',)]) +def test_validate_url_path_no_slash(mock_page_key_to_ini, path): + '''When the path does not begin with a /, + the right error is added to errors + ''' + errors = [] + check_settings.validate_url_path(errors, 'ignored', {'url_path': path}) + + assert len(errors) == 1 + assert isinstance(errors[0], ex.BadFileURLPathError) + + +@pytest.mark.parametrize( + ('path',), [ + ('/',), + ('/foo',)]) +def test_validate_url_path_slash(mock_page_key_to_ini, path): + '''When the path begins with a '/', no error is added to errors + ''' + errors = [] + check_settings.validate_url_path(errors, 'ignored', {'url_path': path}) + + assert len(errors) == 0 + + +mock_validate_url_path = testing.make_mock_fixture( + check_settings, 'validate_url_path') + + +# validate_file_source() + +@pytest.mark.parametrize( + ('source',), [ + ('',), + ('foo',)]) +def test_validate_file_source_no_slash(mock_page_key_to_ini, source): + '''When the source does not begin with a /, + the right error is added to errors + ''' + errors = [] + check_settings.validate_file_source(errors, 'ignored', source) + + assert len(errors) == 1 + assert isinstance(errors[0], ex.BadFileSourceError) + + +@pytest.mark.parametrize( + ('source',), [ + ('/',), + ('/foo',)]) +def test_validate_file_source_slash(mock_page_key_to_ini, source): + '''When the source begins with a '/', no error is added to errors + ''' + errors = [] + check_settings.validate_file_source(errors, 'ignored', source) + + assert len(errors) == 0 + + +mock_validate_file_source = testing.make_mock_fixture( + check_settings, 'validate_file_source') + + +# validate_route_source() + +def test_validate_route_source_empty(mock_page_key_to_ini): + '''When there is no source the right error is added to errors + ''' + errors = [] + check_settings.validate_route_source(errors, 'ignored', '') + + assert len(errors) == 1 + assert isinstance(errors[0], ex.BadRouteSourceError) + + +def test_validate_route_source_not_empty(mock_page_key_to_ini): + '''When there is a source no error is added to errors + ''' + errors = [] + check_settings.validate_route_source(errors, 'ignored', 'something') + + assert len(errors) == 0 + + +mock_validate_route_source = testing.make_mock_fixture( + check_settings, 'validate_route_source') + + +# validate_asset_source() + +def test_validate_asset_source_empty(mock_page_key_to_ini): + '''When there is no source the right error is added to errors + ''' + errors = [] + check_settings.validate_asset_source(errors, 'ignored', '') + + assert len(errors) == 1 + assert isinstance(errors[0], ex.BadAssetSourceError) + + +def test_validate_asset_source_not_empty(mock_page_key_to_ini): + '''When there is a source no error is added to errors + ''' + errors = [] + check_settings.validate_asset_source(errors, 'ignored', 'something') + + assert len(errors) == 0 + + +mock_validate_asset_source = testing.make_mock_fixture( + check_settings, 'validate_asset_source') + + +# validate_file_content() + +@pytest.mark.parametrize( + ('have_settings', 'vup_called'), [ + (True, 1), + (False, 0)]) +def test_validate_file_content( + mock_validate_file_source, mock_require_page_settings, + mock_validate_url_path, + mock_unknown_settings, have_settings, vup_called): + '''validate_file_source() is called, validate_url_path() + is called when settings validate, the unknown_settings() + return value is appended to the errors + ''' + expected_errors = ['some error'] + mock_require_page_settings.return_value = have_settings + mock_unknown_settings.return_value = expected_errors + + errors = [] + check_settings.validate_file_content(errors, None, None, None) + + mock_validate_file_source.assert_called_once() + mock_require_page_settings.assert_called_once() + assert mock_validate_url_path.call_count == vup_called + assert errors == expected_errors + + +mock_validate_file_content = testing.make_mock_fixture( + check_settings, 'validate_file_content') + + +# validate_type_content() + +@pytest.mark.parametrize( + ('page_settings', + 'vus_called', + 'vfc_called', + 'vrs_called', + 'vas_called', + 'pkti_called', + 'error_class'), [ + # URL type + ({'type': 'URL', + 'source': 'ignored'}, + 1, 0, 0, 0, 0, + ex.UnknownSettingKeyError), + # file type + ({'type': 'file', + 'source': 'ignored'}, + 0, 1, 0, 0, 0, + ex.MissingSettingError), + # route type + ({'type': 'route', + 'source': 'ignored'}, + 0, 0, 1, 0, 0, + ex.UnknownSettingKeyError), + # asset type + ({'type': 'asset', + 'source': 'ignored'}, + 0, 0, 0, 1, 0, + ex.UnknownSettingKeyError), + # a unknown type + ({'type': 'unknown', + 'source': 'ignored'}, + 0, 0, 0, 0, 1, + ex.BadPageTypeError)]) +def test_validate_type_content( + mock_validate_url_source, mock_unknown_settings, + mock_validate_file_content, mock_validate_route_source, + mock_validate_asset_source, mock_page_key_to_ini, + page_settings, vus_called, vfc_called, + vrs_called, vas_called, pkti_called, error_class): + '''The expected calls are make, the expected errors returned + ''' + mock_validate_file_content.side_effect = ( + lambda errors, *args: + errors.append(ex.MissingSettingError('ignored'))) + mock_unknown_settings.return_value = [ex.UnknownSettingKeyError( + 'ignored')] + + errors = [] + check_settings.validate_type_content(errors, 'some_page', page_settings) + + assert mock_validate_url_source.call_count == vus_called + assert mock_validate_file_content.call_count == vfc_called + assert mock_validate_asset_source.call_count == vas_called + assert mock_validate_route_source.call_count == vrs_called + assert len(errors) == 1 + assert isinstance(errors[0], error_class) + + +mock_validate_type_content = testing.make_mock_fixture( + check_settings, 'validate_type_content') + + +# validate_page_setting() + +def test_validate_page_setting_nopage( + mock_require_page_settings, mock_validate_type_content): + '''When the page does not have a setting, nothing is done + ''' + errors = [] + settings = {'pgwui': {}} + result = check_settings.validate_page_setting( + errors, settings, 'test_page') + + assert errors == [] + assert result is None + mock_require_page_settings.assert_not_called() + mock_validate_type_content.assert_not_called() + + +def test_validate_page_setting_not_required( + mock_require_page_settings, mock_validate_type_content): + '''When require_page_settings() says something is missing, nothing is done + ''' + errors = [] + settings = {'pgwui': {'test_page': 'ignored'}} + mock_require_page_settings.return_value = False + result = check_settings.validate_page_setting( + errors, settings, 'test_page') + + assert errors == [] + assert result is None + mock_require_page_settings.assert_called_once() + mock_validate_type_content.assert_not_called() + + +def test_validate_page_setting_required( + mock_require_page_settings, mock_validate_type_content): + '''When require_page_settings() says nothing is missing, + validate_type_content() is called + ''' + errors = [] + settings = {'pgwui': {'test_page': 'ignored'}} + mock_require_page_settings.return_value = True + result = check_settings.validate_page_setting( + errors, settings, 'test_page') + + assert errors == [] + assert result is None + mock_require_page_settings.assert_called_once() + mock_validate_type_content.assert_called_once() + + +mock_validate_page_setting = testing.make_mock_fixture( + check_settings, 'validate_page_setting') + + +# validate_settings() + +def test_validate_settings( + mock_validate_setting_values, mock_validate_hmac, + mock_validate_page_setting): + '''The expected calls are made + ''' + check_settings.validate_settings(None, None) + + mock_validate_setting_values.assert_called_once() + mock_validate_hmac.assert_called_once() + assert mock_validate_page_setting.call_count == 2 -- 2.34.1