Move validation from pgwui_server to pgwui_common
authorKarl O. Pinc <kop@karlpinc.com>
Mon, 7 Dec 2020 22:33:54 +0000 (16:33 -0600)
committerKarl O. Pinc <kop@karlpinc.com>
Mon, 7 Dec 2020 22:33:54 +0000 (16:33 -0600)
README.rst
src/pgwui_common/check_settings.py [new file with mode: 0644]
src/pgwui_common/constants.py [new file with mode: 0644]
src/pgwui_common/exceptions.py
tests/test_check_settings.py [new file with mode: 0644]

index eff18f6f3aad1c5a93b47470f53311cfc606da50..cc52ba436c83187bcba1f7b522669ee363562e67 100644 (file)
@@ -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 (file)
index 0000000..1cdc1e2
--- /dev/null
@@ -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
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+'''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 (file)
index 0000000..100fe56
--- /dev/null
@@ -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
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+'''Constants shared by multiple modules
+'''
+
+
+# Required length of HMAC value
+HMAC_LEN = 40
index b91e9c9a5e6ff409ec2decf01779a5228e7279dc..3ab67841bf91dfe3db872e1da133bad3bf7f29e7 100644 (file)
@@ -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 (file)
index 0000000..9e08140
--- /dev/null
@@ -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
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+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