convenient when writing your own components. (It also eliminates the
trivial overhead involved in autoconfiguration.)
+Configuring Navbar Links
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Navbar link configuration is optional. PGWUI comes with sensible
+defaults.
+
+ pgwui.home_page:: How to link to the site's home page.
+ type: URL (default), asset, file
+
+ URL
+ source:
+
+ The default is ``/``, when there is no home_page setting.
+ Which produces an URL with no "path".
+
+ * A URI path beginning with ``/``. E.g.: '/home'
+
+ * An URL without a protocol, so an URL beginning with ``//`` and
+ followed by a domain. E.g.: //www.example.com The URL
+ delivered to the browser contains the protocol used in the request.
+
+ * An URL with a protocol. E.g.: https://www.example.com
+
+ file:
+ source:
+ A fully-qualified file system path, so a path beginning with
+ a ``/``. E.g. /var/www/html/index.html
+ Served with a content encoding of ``text/html``.
+
+ asset:
+ source:
+ * A `Pyramid`_ `asset specification`_. It must reference a
+ `static asset`_, a file included in a `Pyramid`_ application.
+ Typically file containing a page of HTML.
+
+ This is only useful to users who write their own `Pyramid`_
+ applications that are either PGWUI modules or incorporate
+ PGWUI.
+
+ route:
+ source:
+ * A `Pyramid`_ `route name`_. Used to reach a page generated
+ by a `Pyramid`_ application which uses `URL dispatch`_.
+
+ This is only useful to users who write their own `Pyramid`_
+ applications that incorporate PGWUI.
+
+ pgwui.menu_page:: How to link to a menu of PGWUI components.
+ All of the "type"s of ``pgwui.home_page`` are available.
+
+
+Configuration Settings Common to All PGWUI Components
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+PGWUI modules all have the following configuration settings:
+
+ menu_label
+ The label for PGWUI_Menu to display, when different from the default
+
+
Configuring Routing
^^^^^^^^^^^^^^^^^^^
control what is displayed.
Here is a list of the current asset specifications with brief
-descriptions::
+descriptions:
- pgwui_common:static/pgwui.css The CSS file for PGWUI.
+ pgwui_common:static/pgwui.css
+ The CSS file for PGWUI.
- pgwui_common:templates/base.mak Common "background" items on all pages.
+ pgwui_common:templates/base.mak
+ Common "background" items on all pages.
- pgwui_common:templates/auth_base.mak Common "background" items on all
- pages requesting database connection and login information.
+ pgwui_common:templates/auth_base.mak
+ Common "background" items on all
+ pages requesting database connection and login information.
- pgwui_logout:templates/logout.mak The logout page.
+ pgwui_logout:templates/logout.mak
+ The logout page.
- pgwui_upload:templates/upload.mak The upload page.
+ pgwui_upload:templates/upload.mak
+ The upload page.
Assets can be overridden in the configuration file with
-``pgwui.override_asset``:
+``pgwui.override_asset``::
pgwui.override_asset =
# Syntax is: asset_to_override = override_with
.. _WSGI: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
.. _pip: https://pip.pypa.io/en/stable/
+.. _asset specification: https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/assets.html#understanding-asset-specifications
+.. _static asset: https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/assets.html#serving-static-assets
+.. _route name: https://docs.pylonsproject.org/projects/pyramid/en/1.10-branch/narr/urldispatch.html#route-configuration
+.. _URL dispatch: https://docs.pylonsproject.org/projects/pyramid/en/1.10-branch/narr/urldispatch.html
+
.. rubric:: Footnotes
# PGWUI configuration
#
-# Postgres client configuration.
+# Postgres connection configuration.
# Both pgwui.pg_host and pgwui.pg_port are optional;
# they default to the Unix PG socket and the default Postgres port.
# There are occasions when PGWUI uses a default database. (optional)
pgwui.default_db = template1
+
+# How to link to the site's home page.
+#
+# This is a multi-valued setting. The keys depend on the "type"
+# used.
+#
+# type:
+# URL An URL. This is the default.
+# file A file containing HTML, read from the file system.
+# asset A Pyramid asset specification. Results in an URL
+# which references a file which is part of a PGWUI
+# component, or some other "static" file included in a Pyramid
+# application.
+# route A Pyramid route name. Results in an URL
+# which references a page generated by a Pyramid application.
+# "pgwui.route_prefix" is not applied.
+#
+#
+# When "type" is "URL, there are the following keys:
+#
+# source: (required)
+#
+# The default is ``/``, when there is no home_page setting.
+# Which produces an URL with no "path".
+#
+# * A URI path beginning with ``/``. E.g.: '/home'
+#
+# * An URL without a protocol, so an URL beginning with ``//`` and
+# followed by a domain. E.g.: //www.example.com The URL
+# delivered to the browser contains the protocol used in the request.
+#
+# * An URL with a protocol. E.g.: https://www.example.com
+#
+#
+# When "type" is "file", there are the following keys
+#
+# source: (required)
+# A fully-qualified file system path, so a path beginning with
+# a ``/``. E.g. /var/www/html/index.html
+# Served with a content encoding of ``text/html``.
+#
+# url_path: (required)
+# The "path" component of the URL used to retrieve the file.
+# Must begin with a ``/``. "pgwui.route_prefix" is not applied.
+#
+# When type is "asset", there are the following keys:
+#
+# source: (required)
+#
+# * A `Pyramid`_ `asset specification`_. It must reference a
+# `static asset`_, a file included in a `Pyramid`_ application.
+# A file containing a page of HTML.
+#
+#
+# When type is "route", there are the following keys:
+#
+# source: (required)
+#
+# * A `Pyramid`_ `route name`_. Used to reach a page generated
+# by a `Pyramid`_ application which uses `URL dispatch`_.
+#
+#
+# pgwui.home_page =
+# type = URL
+# source = /
+
+# How to link to a menu of PGWUI components. An alternative to using
+# the PGWUI_Menu component.
+# Configured as pgwui.home_page is configured, above.
+# The default is to have no menu. The following uses the URL
+# without a path (e.g., http://www.example.com/) as the menu page.
+#
+# pgwui.menu_page =
+# type = URL
+# source = /
+#
+# pgwui.menu_page overrides what is provided by the PGWUI_Menu component.
+
# Whether to auto-discover the pgwui component modules. (optional)
# When False pgwui component names must be listed in pyramid.includes=...
# pgwui.autoconfigure = True
# Routing
-# Routes are what call up specific pages. They are the
+# Routes are what call up specific pages. They are usually the
# part of the URL which comes after the http://example.com.
#
# All routes should probably begin with a "/" character but
#
# The default for some PGWUI components are:
# pgwui.routes =
-# pgwui_logout = /logmeout
-# pgwui_upload = /put-in
+# pgwui_logout = /logout
+# pgwui_upload = /upload
# Settings validation
# vulnerabilties. Validation is on by default.
# pgwui.validate_hmac = True
+
# PGWUI Component Settings
# Menu presentation
# PGWUI configuration
#
-# Postgres client configuration.
+# Postgres connection configuration.
# Both pgwui.pg_host and pgwui.pg_port are optional;
# they default to the Unix PG socket and the default Postgres port.
pgwui.default_db = template1
+# How to link to the site's home page. Useful when the home page is
+# not the PGWUI menu.
+#
+# This is a multi-valued setting. The keys depend on the "type"
+# used.
+#
+# type:
+# URL An URL. This is the default.
+# file A file containing HTML, read from the file system.
+# asset A Pyramid asset specification. Results in an URL
+# which references a file which is part of a PGWUI
+# component, or some other "static" file included in a Pyramid
+# application.
+# route A Pyramid route name. Results in an URL
+# which references a page generated by a Pyramid application.
+# "pgwui.route_prefix" is not applied.
+#
+#
+# When "type" is "URL, there are the following keys:
+#
+# source: (required)
+#
+# The default is ``/``, when there is no home_page setting.
+# Which produces an URL with no "path".
+#
+# * A URI path beginning with ``/``. E.g.: '/home'
+#
+# * An URL without a protocol, so an URL beginning with ``//`` and
+# followed by a domain. E.g.: //www.example.com The URL
+# delivered to the browser contains the protocol used in the request.
+#
+# * An URL with a protocol. E.g.: https://www.example.com
+#
+#
+# When "type" is "file", there are the following keys
+#
+# source: (required)
+# A fully-qualified file system path, so a path beginning with
+# a ``/``. E.g. /var/www/html/index.html
+# Served with a content encoding of ``text/html``.
+#
+# url_path: (required)
+# The "path" component of the URL used to retrieve the file.
+# Must begin with a ``/``. "pgwui.route_prefix" is not applied.
+#
+# When type is "asset", there are the following keys:
+#
+# source: (required)
+#
+# * A `Pyramid`_ `asset specification`_. It must reference a
+# `static asset`_, a file included in a `Pyramid`_ application.
+# A file containing a page of HTML.
+#
+#
+# When type is "route", there are the following keys:
+#
+# source: (required)
+#
+# * A `Pyramid`_ `route name`_. Used to reach a page generated
+# by a `Pyramid`_ application which uses `URL dispatch`_.
+#
+#
+# pgwui.home_page =
+# type = URL
+# source = /
+
+
+# How to link to a menu of PGWUI components. An alternative to using
+# the PGWUI_Menu component.
+# Configured as pgwui.home_page is configured, above.
+# The default is to have no menu. The following uses the URL
+# without a path (e.g., http://www.example.com/) as the menu page.
+#
+# pgwui.menu_page =
+# type = URL
+# source = /
+#
+# pgwui.menu_page overrides what is provided by the PGWUI_Menu component.
+
# Whether to auto-discover the pgwui component modules. (optional)
# When False pgwui component names must be listed in pyramid.includes=...
# pgwui.autoconfigure = True
# What PGWUI components and other pyramid modules to use.
-# (Required when pgwui.autoconfigure is False.)
+# (Required when pgwui.autoconfigure is False, or you want the
+# debug toolbar.)
+#pyramid.includes =
+# pgwui_logout
+# pgwui_upload
pyramid.includes =
pyramid_debugtoolbar
# vulnerabilties. Validation is on by default.
pgwui.validate_hmac = False
+
# PGWUI Component Settings
# Menu presentation
# HMAC secret
#session.secret = xxxxxxrandomstring40characterslongxxxxxx
# Send cookie only over https
-# (True for production)
# WARNING: To use HTTP, not HTTPS, session.secure must be False!
# CAUTION: If you are forcing the browser to use HTTPS you want
# session.secure to be True.
# Karl O. Pinc <kop@karlpinc.com>
-'''Check the pgwui settings (in the internal "dict format")
+'''Validate PGWUI_Core and PGWUI_Common configuration
'''
+import re
from ast import literal_eval
from . import constants
from pgwui_common import exceptions as common_ex
+from pgwui_common import checkset
import pgwui_server.exceptions as server_ex
+# 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)
+ return 'pgwui:{}'.format(key)
-def require_setting(errors, setting, pgwui_settings):
+def require_setting(errors, setting, pgwui_settings, formatter):
if setting not in pgwui_settings:
- errors.append(common_ex.MissingSettingError(key_to_ini(setting)))
+ errors.append(common_ex.MissingSettingError(formatter(setting)))
+ return False
+ return True
def boolean_setting(errors, setting, pgwui_settings):
# default_db can be missing, then the user sees no default
# dry_run
- require_setting(errors, 'dry_run', pgwui_settings)
+ 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.
if len(settings['session.secret']) != constants.HMAC_LEN:
errors.append(server_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(common_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(common_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(common_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(common_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(common_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(common_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)
'Autoconfigure is True and there is a pyramid.include setting')
+class MenuPageInRoutes(ServerError):
+ def __init__(self):
+ super().__init__(
+ 'The pgwui_menu in the pgwui.routes setting is ignored '
+ 'and the pgwui.menu_page setting used instead')
+
+
class BadSettingsAbort(ServerError):
def __init__(self):
super().__init__('Aborting due to bad setting(s)')
# Karl O. Pinc <kop@karlpinc.com>
-'''Provide a way to configure PGWUI.
+'''Load the PGWUI components, parse the PGWUI configuration, and start the
+WSGI server.
'''
from pyramid.config import Configurator
# Constants
-# All the settings recognized by PGWUI
+# All the single-valued settings recognized by PGWUI_Server/Core
SETTINGS = set(
['pg_host',
'pg_port',
'autoconfigure',
])
+# All the multi-valued settings recognized by PGWUI_Server/Core
+MULTI_SETTINGS = set(
+ ['home_page',
+ 'menu_page',
+ ])
+
+# Default settings
+DEFAULT_HOME_PAGE_TYPE = 'URL'
+DEFAULT_HOME_PAGE_SOURCE = '/'
+DEFAULT_SETTINGS = { # As delivered by configparser to this parser
+ 'pgwui.home_page': f'type = {DEFAULT_HOME_PAGE_TYPE}\n'
+ f'source = {DEFAULT_HOME_PAGE_SOURCE}\n'}
# Logging
log = logging.getLogger(__name__)
# Functions
-def dot_to_component_settings(settings, key, component):
- '''Put a component's settings into its own dict,
- adding to what's already there
- '''
- comp_settings = settings['pgwui'].setdefault(component, dict())
- comp_settings.update(settings[key])
- del settings[key]
-
-
-def component_setting_into_dict(
- errors, component_checkers, key, settings, component):
- '''Put a component's settings in its own dict and validate them
- '''
- comp_settings = dot_to_component_settings(settings, key, component)
- if component in component_checkers:
- errors.extend(
- component_checkers[component](comp_settings))
-
-
def dot_to_dict(settings, key, new_key):
settings['pgwui'][new_key] = settings[key]
del settings[key]
return result
+def dot_to_multiline_setting(settings, key, pgwui_key):
+ '''Put a multi-line setting into its own dict,
+ adding to what's already there
+ '''
+ multi_setting = settings['pgwui'].setdefault(pgwui_key, dict())
+ multi_setting.update(dict(parse_assignments(settings[key])))
+ del settings[key]
+
+
+def component_setting_into_dict(
+ errors, component_checkers, key, settings, component):
+ '''Put a component's settings in its own dict and validate them
+ '''
+ comp_settings = dot_to_multiline_setting(settings, key, component)
+ if component in component_checkers:
+ errors.extend(
+ component_checkers[component](comp_settings))
+
+
def setting_into_dict(
errors, components, component_checkers, key, settings):
'''Separate a pgwui setting into a dict on '.' chars; validate
if key[:6] == 'pgwui.':
new_key = key[6:]
if new_key in components:
- settings[key] = dict(parse_assignments(settings[key]))
component_setting_into_dict(
errors, component_checkers, key, settings, new_key)
else:
if new_key in SETTINGS:
dot_to_dict(settings, key, new_key)
+ elif new_key in MULTI_SETTINGS:
+ dot_to_multiline_setting(settings, key, new_key)
else:
errors.append(common_ex.UnknownSettingKeyError(key))
def dictify_settings(errors, settings, components):
- '''Convert . in the pgwui settings to dict mappings, and validate
+ '''Convert "." in the pgwui settings to dict mappings, and validate
the result.
'''
component_checkers = plugin.find_pgwui_check_settings()
errors, components, component_checkers, key, settings)
checkset.validate_setting_values(errors, settings)
checkset.validate_hmac(errors, settings)
+ checkset.validate_page_setting(errors, settings, 'home_page')
+ checkset.validate_page_setting(errors, settings, 'menu_page')
def exit_reporting_errors(errors):
sys.exit(1)
+def add_default_settings(settings):
+ '''Add the default settings to the config if not there
+ '''
+ for setting, val in DEFAULT_SETTINGS.items():
+ settings.setdefault(setting, val)
+
+
def exit_on_invalid_settings(settings, components):
'''Exit when settings don't validate
'''
+ add_default_settings(settings)
errors = []
dictify_settings(errors, settings, components)
if errors:
'''
pgwui_settings = settings['pgwui']
if 'routes' in pgwui_settings:
+ menu_page = 'menu_page' in pgwui_settings
routes = parse_assignments(pgwui_settings['routes'])
for name, route in routes:
- config.add_route(name, route)
+ if menu_page and name == 'pgwui_menu':
+ log.info(server_ex.MenuPageInRoutes())
+ else:
+ config.add_route(name, route)
def autoconfigurable_components(settings, components):
import pytest
import pgwui_common.exceptions as common_ex
+import pgwui_common
from pgwui_server import checkset
import pgwui_server.constants as constants
from pgwui_server import exceptions as server_ex
from pgwui_testing import testing
+mock_unknown_settings = testing.make_mock_fixture(
+ pgwui_common.checkset, 'unknown_settings')
+
# key_to_ini()
key = 'pgwui_example'
result = checkset.key_to_ini(key)
- assert result == 'pgwui.' + key
+ assert result == 'pgwui:' + key
mock_key_to_ini = testing.make_mock_fixture(
def test_require_setting_missing():
'''Deliver exception when a required setting is missing'''
errors = []
- checkset.require_setting(errors, 'key', {})
+ checkset.require_setting(errors, 'key', {}, lambda x: x)
assert errors
assert isinstance(errors[0], common_ex.MissingSettingError)
def test_require_setting_present():
'''Does nothing when a required setting is present'''
errors = []
- checkset.require_setting(errors, 'key', {'key': 'value'})
+ checkset.require_setting(errors, 'key', {'key': 'value'}, lambda x: x)
assert errors == []
mock_validate_hmac = testing.make_mock_fixture(
checkset, 'validate_hmac')
+
+
+# page_key_to_ini()
+
+@pytest.mark.unittest
+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 = checkset.page_key_to_ini(None, None)
+ assert result == 'foo'
+
+
+mock_page_key_to_ini = testing.make_mock_fixture(
+ checkset, '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)])
+@pytest.mark.unittest
+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 = checkset.require_page_settings(
+ None, required_settings, None, None)
+ assert result == expected
+
+
+@pytest.mark.unittest
+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
+ checkset.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(
+ checkset, '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
+ ('//', common_ex.BadURLSourceError),
+ # Nothing
+ ('', common_ex.BadURLSourceError),
+ # Missing / after scheme
+ ('http:/www.example.com', common_ex.BadURLSourceError),
+ # Extra / after scheme
+ ('http:///www.example.com', common_ex.BadURLSourceError)])
+@pytest.mark.unittest
+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 = []
+ checkset.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(
+ checkset, 'validate_url_source')
+
+
+# validate_url_path()
+
+@pytest.mark.parametrize(
+ ('path',), [
+ ('',),
+ ('foo',)])
+@pytest.mark.unittest
+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 = []
+ checkset.validate_url_path(errors, 'ignored', {'url_path': path})
+
+ assert len(errors) == 1
+ assert isinstance(errors[0], common_ex.BadFileURLPathError)
+
+
+@pytest.mark.parametrize(
+ ('path',), [
+ ('/',),
+ ('/foo',)])
+@pytest.mark.unittest
+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 = []
+ checkset.validate_url_path(errors, 'ignored', {'url_path': path})
+
+ assert len(errors) == 0
+
+
+mock_validate_url_path = testing.make_mock_fixture(
+ checkset, 'validate_url_path')
+
+
+# validate_file_source()
+
+@pytest.mark.parametrize(
+ ('source',), [
+ ('',),
+ ('foo',)])
+@pytest.mark.unittest
+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 = []
+ checkset.validate_file_source(errors, 'ignored', source)
+
+ assert len(errors) == 1
+ assert isinstance(errors[0], common_ex.BadFileSourceError)
+
+
+@pytest.mark.parametrize(
+ ('source',), [
+ ('/',),
+ ('/foo',)])
+@pytest.mark.unittest
+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 = []
+ checkset.validate_file_source(errors, 'ignored', source)
+
+ assert len(errors) == 0
+
+
+mock_validate_file_source = testing.make_mock_fixture(
+ checkset, 'validate_file_source')
+
+
+# validate_route_source()
+
+@pytest.mark.unittest
+def test_validate_route_source_empty(mock_page_key_to_ini):
+ '''When there is no source the right error is added to errors
+ '''
+ errors = []
+ checkset.validate_route_source(errors, 'ignored', '')
+
+ assert len(errors) == 1
+ assert isinstance(errors[0], common_ex.BadRouteSourceError)
+
+
+@pytest.mark.unittest
+def test_validate_route_source_not_empty(mock_page_key_to_ini):
+ '''When there is a source no error is added to errors
+ '''
+ errors = []
+ checkset.validate_route_source(errors, 'ignored', 'something')
+
+ assert len(errors) == 0
+
+
+mock_validate_route_source = testing.make_mock_fixture(
+ checkset, 'validate_route_source')
+
+
+# validate_asset_source()
+
+@pytest.mark.unittest
+def test_validate_asset_source_empty(mock_page_key_to_ini):
+ '''When there is no source the right error is added to errors
+ '''
+ errors = []
+ checkset.validate_asset_source(errors, 'ignored', '')
+
+ assert len(errors) == 1
+ assert isinstance(errors[0], common_ex.BadAssetSourceError)
+
+
+@pytest.mark.unittest
+def test_validate_asset_source_not_empty(mock_page_key_to_ini):
+ '''When there is a source no error is added to errors
+ '''
+ errors = []
+ checkset.validate_asset_source(errors, 'ignored', 'something')
+
+ assert len(errors) == 0
+
+
+mock_validate_asset_source = testing.make_mock_fixture(
+ checkset, 'validate_asset_source')
+
+
+# validate_file_content()
+
+@pytest.mark.parametrize(
+ ('have_settings', 'vup_called'), [
+ (True, 1),
+ (False, 0)])
+@pytest.mark.unittest
+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 = []
+ checkset.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(
+ checkset, '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,
+ common_ex.UnknownSettingKeyError),
+ # file type
+ ({'type': 'file',
+ 'source': 'ignored'},
+ 0, 1, 0, 0, 0,
+ common_ex.MissingSettingError),
+ # route type
+ ({'type': 'route',
+ 'source': 'ignored'},
+ 0, 0, 1, 0, 0,
+ common_ex.UnknownSettingKeyError),
+ # asset type
+ ({'type': 'asset',
+ 'source': 'ignored'},
+ 0, 0, 0, 1, 0,
+ common_ex.UnknownSettingKeyError),
+ # a unknown type
+ ({'type': 'unknown',
+ 'source': 'ignored'},
+ 0, 0, 0, 0, 1,
+ common_ex.BadPageTypeError)])
+@pytest.mark.unittest
+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(common_ex.MissingSettingError('ignored')))
+ mock_unknown_settings.return_value = [common_ex.UnknownSettingKeyError(
+ 'ignored')]
+
+ errors = []
+ checkset.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(
+ checkset, 'validate_type_content')
+
+
+# validate_page_setting()
+
+@pytest.mark.unittest
+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 = checkset.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()
+
+
+@pytest.mark.unittest
+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 = checkset.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()
+
+
+@pytest.mark.unittest
+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 = checkset.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()
# Unit tests
-# dot_to_component_settings()
+# dot_to_multiline_setting()
@pytest.mark.unittest
-def test_dot_to_component_settings_new():
+def test_dot_to_multiline_setting_new(mock_parse_assignments):
'''Adds a new dict and puts the settings in it
'''
comp_settings = {'foo': 'foo', 'bar': 'bar'}
key: comp_settings}
expected = {'pgwui': {component: comp_settings}}
- pgwui_server.dot_to_component_settings(
+ mock_parse_assignments.return_value = comp_settings
+ pgwui_server.dot_to_multiline_setting(
settings, key, 'pgwui_component')
assert settings == expected
@pytest.mark.unittest
-def test_dot_to_component_settings_old():
+def test_dot_to_multiline_setting_old(mock_parse_assignments):
'''Extends an existing dict in the settings
'''
comp_settings = {'foo': 'foo', 'bar': 'bar'}
expected = {'pgwui':
{component: {'foo': 'foo', 'bar': 'bar', 'baz': 'baz'}}}
- pgwui_server.dot_to_component_settings(
+ mock_parse_assignments.return_value = comp_settings
+ pgwui_server.dot_to_multiline_setting(
settings, key, 'pgwui_component')
assert settings == expected
-mock_dot_to_component_setting = testing.make_mock_fixture(
- pgwui_server, 'dot_to_component_settings')
+mock_dot_to_multiline_setting = testing.make_mock_fixture(
+ pgwui_server, 'dot_to_multiline_setting')
# component_setting_into_dict()
@pytest.mark.unittest
def test_component_setting_into_dict_no_checker(
- mock_dot_to_component_setting):
+ mock_dot_to_multiline_setting):
'''When there's no checker nothing is done
'''
errors = []
@pytest.mark.unittest
def test_component_setting_into_dict_checker(
- mock_dot_to_component_setting):
+ mock_dot_to_multiline_setting):
'''When there's a checker its result is appended to the errors
'''
errors = ['someerror']
@pytest.mark.unittest
def test_setting_into_dict_unknown(
- mock_parse_assignments,
mock_component_setting_into_dict,
- mock_dot_to_dict):
+ mock_dot_to_dict,
+ mock_dot_to_multiline_setting):
'''No new errors when there's a non-pgwui setting'''
errors = []
pgwui_server.setting_into_dict(errors, [], {}, 'foo', {})
def test_setting_into_dict_bad(
mock_parse_assignments,
mock_component_setting_into_dict,
- mock_dot_to_dict):
+ mock_dot_to_dict,
+ mock_dot_to_multiline_setting):
'''Delivers an error on a bad pgwui setting'''
errors = []
@pytest.mark.unittest
def test_setting_into_dict_good(
- mock_parse_assignments,
mock_component_setting_into_dict,
- mock_dot_to_dict):
- '''Calls dot_to_dict when a known pgwui setting is supplied'''
+ mock_dot_to_dict,
+ mock_dot_to_multiline_setting):
+ '''Calls dot_to_dict when a known pgwui setting is supplied
+ '''
errors = []
pgwui_server.setting_into_dict(
errors, [], {}, 'pgwui.pg_host', {})
mock_dot_to_dict.assert_called_once()
+ mock_dot_to_multiline_setting.assert_not_called()
+ assert errors == []
+
+
+@pytest.mark.unittest
+def test_setting_into_dict_multiline(
+ mock_component_setting_into_dict,
+ mock_dot_to_dict,
+ mock_dot_to_multiline_setting):
+ '''Calls dot_to_multiline_setting when a known pgwui multi-line
+ setting is supplied
+ '''
+ errors = []
+
+ pgwui_server.setting_into_dict(
+ errors, [], {}, 'pgwui.home_page', {})
+
+ mock_dot_to_dict.assert_not_called()
+ mock_dot_to_multiline_setting.assert_called_once()
assert errors == []
@pytest.mark.unittest
def test_setting_into_dict_plugin_component(
- mock_parse_assignments,
mock_component_setting_into_dict,
- mock_dot_to_dict):
+ mock_dot_to_dict,
+ mock_dot_to_multiline_setting):
'''When a setting is for a component the setting is parsed and
moved into a dict
'''
pgwui_server.setting_into_dict(
errors, ['pgwui_component'], {}, key, settings)
- mock_parse_assignments.assert_called_once()
mock_component_setting_into_dict.assert_called_once()
assert errors == []
pgwui_server, 'exit_reporting_errors')
+# add_default_settings()
+
+@pytest.mark.unittest
+def test_add_default_settings():
+ '''The default settings are added
+ '''
+ settings = dict()
+ pgwui_server.add_default_settings(settings)
+
+ assert settings == pgwui_server.DEFAULT_SETTINGS
+
+
+mock_add_default_settings = testing.make_mock_fixture(
+ pgwui_server, 'add_default_settings')
+
+
# exit_on_invalid_settings()
@pytest.mark.unittest
-def test_exit_on_invalid_settings_invalid(monkeypatch,
- mock_exit_reporting_errors):
+def test_exit_on_invalid_settings_invalid(
+ monkeypatch,
+ mock_add_default_settings, mock_dictify_settings,
+ mock_exit_reporting_errors):
'''Calls dictify_settings and exit_reporting_errors() when
setting is invalid
'''
- def mock_dictify_settings(errors, settings, components):
+ def mymock(errors, settings, components):
errors.append('error1')
- monkeypatch.setattr(pgwui_server, 'dictify_settings',
- mock_dictify_settings)
+ mock_dictify_settings.side_effect = mymock
pgwui_server.exit_on_invalid_settings({}, [])
+ mock_dictify_settings.assert_called_once()
+ mock_add_default_settings.assert_called_once()
assert mock_exit_reporting_errors.called
@pytest.mark.unittest
-def test_exit_on_invalid_settings_valid(mock_dictify_settings):
+def test_exit_on_invalid_settings_valid(
+ mock_add_default_settings, mock_dictify_settings,
+ mock_exit_reporting_errors):
'''Returns, without exiting, when all settings are valid
'''
pgwui_server.exit_on_invalid_settings({}, [])
assert mocked_add_route.call_count == len(test_routes)
+@pytest.mark.unittest
+def test_add_routes_menu(mock_add_route, mock_parse_assignments, caplog):
+ '''When there is a a route for pgwui_menu, but there is a menu_page
+ setting, no route is added and an INFO message is logged
+ '''
+ caplog.set_level(logging.DEBUG)
+
+ test_routes = [('pgwui_menu', 'notused')]
+ mock_parse_assignments.return_value = test_routes
+ with pyramid.testing.testConfig() as config:
+ mocked_add_route = mock_add_route(config)
+ pgwui_server.add_routes(config, {'pgwui': {'routes': 'notused',
+ 'menu_page': 'anything'}})
+
+ mocked_add_route.assert_not_called()
+
+ logs = caplog.record_tuples
+ assert len(logs) == 1
+ assert logs[0][1] == logging.INFO
+
+
mock_add_routes = testing.make_mock_fixture(
pgwui_server, 'add_routes')
# apply_component_defaults()
-
@pytest.mark.unittest
def test_apply_component_defaults(monkeypatch, caplog,
mock_autoconfigurable_components,