From 2204360f040d5c4c1f96a5ce298a5f8477c9bda6 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 8 Dec 2020 11:58:49 -0600 Subject: [PATCH] Return error(s) when page routes or assets do not resolve --- README.rst | 4 ++ src/pgwui_common/assets.py | 38 +++++++++++++++++ src/pgwui_common/exceptions.py | 36 +++++++++------- src/pgwui_common/urls.py | 27 +++++++++--- tests/test_assets.py | 60 +++++++++++++++++++++++++++ tests/test_urls.py | 76 ++++++++++++++++++++++++++++------ 6 files changed, 206 insertions(+), 35 deletions(-) create mode 100644 src/pgwui_common/assets.py create mode 100644 tests/test_assets.py diff --git a/README.rst b/README.rst index cc52ba4..d06e4dc 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,9 @@ PGWUI_Common provides: * Code used to establish `routes`_, called by PGWUI_Server or whatever else is used to configure `Pyramid`_. + * Code used to override `assets`_, 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. @@ -193,3 +196,4 @@ provided by `The Dian Fossey Gorilla Fund .. _Pyramid: https://trypyramid.com/ .. _Pyramid's: `Pyramid`_ .. _routes: https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html +.. _assets: https://docs.pylonsproject.org/projects/pyramid/en/1.10-branch/narr/assets.html diff --git a/src/pgwui_common/assets.py b/src/pgwui_common/assets.py new file mode 100644 index 0000000..248d37f --- /dev/null +++ b/src/pgwui_common/assets.py @@ -0,0 +1,38 @@ +# Copyright (C) 2018, 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 + +'''Override assets based on settings +''' + +import logging + +# Logging +log = logging.getLogger(__name__) + + +def override_assets(config, settings): + pgwui = settings['pgwui'] + if 'override_assets' not in pgwui: + return + + for asset, replacement in pgwui['override_assets'].items(): + config.override_asset(asset, replacement) + log.debug(f'Overriding asset ({asset}) with ({replacement})') diff --git a/src/pgwui_common/exceptions.py b/src/pgwui_common/exceptions.py index 3ab6784..cd69831 100644 --- a/src/pgwui_common/exceptions.py +++ b/src/pgwui_common/exceptions.py @@ -125,6 +125,26 @@ class NotBooleanSettingError(Error): .format(key)) +class BadPathError(Error): + pass + + +class BadRouteError(BadPathError): + def __init__(self, page, ex): + super().__init__( + page, ex, + f'The "pgwui:{page}:source" configuration setting refers to ' + 'a route that does not exist') + + +class BadAssetError(BadPathError): + def __init__(self, page, ex): + super().__init__( + page, ex, + f'The "pgwui:{page}:source" configuration setting refers to ' + 'an asset that does not exist') + + class ViewError(Error): pass @@ -159,19 +179,3 @@ class BadPageIsADirectoryError(BadPageError): page, ex, f'The "pgwui:{page}:source" configuration setting refers to ' f'a directory ({ex.filename}), not a file') - - -class BadRouteError(BadPageError): - def __init__(self, page, ex): - super().__init__( - page, ex, - f'The "pgwui:{page}:source" configuration setting refers to ' - 'a route that does not exist') - - -class BadAssetError(BadPageError): - def __init__(self, page, ex): - super().__init__( - page, ex, - f'The "pgwui:{page}:source" configuration setting refers to ' - 'an asset that does not exist') diff --git a/src/pgwui_common/urls.py b/src/pgwui_common/urls.py index 63ae9cd..fa5fb98 100644 --- a/src/pgwui_common/urls.py +++ b/src/pgwui_common/urls.py @@ -76,15 +76,18 @@ def set_menu_url(request, urls): try: menu_url = request.route_path('pgwui_menu') except KeyError: - return + return [] # the pgwui_menu component is optional + except ex.BadPathError as exp: + return [exp] if menu_url != urls['pgwui_home']: urls['pgwui_menu'] = menu_url + return [] def set_component_urls(request, urls): '''Add urls for each pgwui component to the 'urls' dict ''' - set_menu_url(request, urls) + errors = set_menu_url(request, urls) components = find_pgwui_components() if 'pgwui_menu' in components: components.remove('pgwui_menu') @@ -97,13 +100,24 @@ def set_component_urls(request, urls): else: urls.setdefault(component, url) + return errors + def set_urls(request, urls): '''Build 'urls' dict with all the urls ''' - home_url = url_of_page(request, 'home_page') - urls.setdefault('pgwui_home', home_url) - set_component_urls(request, urls) + errors = [] + try: + home_url = url_of_page(request, 'home_page') + except ex.BadPathError as exp: + errors.append(exp) + # set_component_urls() requires a 'pgwui_home' key, and the error + # means the program will exit and the value never be used. + urls.setdefault('pgwui_home', None) + else: + urls.setdefault('pgwui_home', home_url) + errors.extend(set_component_urls(request, urls)) + return errors def add_urls_setting(config, settings): @@ -115,7 +129,8 @@ def add_urls_setting(config, settings): # be installed in a production enviornment. And some RAM. request = pyramid.request.Request.blank('/') request.registry = config.registry - set_urls(request, urls) + errors = set_urls(request, urls) settings['pgwui']['urls'] = urls log.debug('Routing map of route names to url paths which is given' f' to the templates: {urls}') + return errors diff --git a/tests/test_assets.py b/tests/test_assets.py new file mode 100644 index 0000000..9650edf --- /dev/null +++ b/tests/test_assets.py @@ -0,0 +1,60 @@ +# Copyright (C) 2018, 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 logging +import pgwui_common.assets as assets + +from pgwui_testing import testing + +# Activiate the PGWUI pytest plugin +pytest_plugins = ("pgwui",) + +# Mark all tests with "unittest" +pytestmark = pytest.mark.unittest + +mock_override_asset = testing.instance_method_mock_fixture('override_asset') + + +# override_asset() + +@pytest.mark.parametrize( + ('overrides', 'call_count'), [ + ({}, 0), + ({'some_asset': 'new_asset'}, 1), + ({'some_asset': 'new_asset', + 'other_asset': 'other new asset'}, 2)]) +def test_override_asset( + caplog, pyramid_config, mock_override_asset, overrides, call_count): + '''override_asset() is called for each override + ''' + caplog.set_level(logging.DEBUG) + mocked_override_asset = mock_override_asset(pyramid_config) + + if overrides: + settings = {'override_assets': overrides} + else: + settings = {} + assets.override_assets(pyramid_config, {'pgwui': settings}) + + assert mocked_override_asset.call_count == call_count + assert len(caplog.record_tuples) == call_count diff --git a/tests/test_urls.py b/tests/test_urls.py index 1bc4913..5c854ef 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -183,7 +183,7 @@ mock_url_of_page = testing.make_mock_fixture( # set_menu_url() @pytest.mark.parametrize( - "test_urls,expected", + ('test_urls', 'expected_urls'), [ # menu and home have identical urls, no url is added for menu ({'pgwui_menu': '/', 'pgwui_home': '/'}, @@ -194,10 +194,11 @@ mock_url_of_page = testing.make_mock_fixture( # menu and home have different urls, url is added for menu ({'pgwui_menu': '/menu', 'pgwui_home': '/'}, {'pgwui_menu': '/menu'})]) -def test_set_menu_url( +def test_set_menu_url_good_path( pyramid_request_config, mock_method_route_path, mock_url_of_page, - test_urls, expected): - '''The expected urls are returned + test_urls, expected_urls): + '''The expected urls are returned without errors when the path + settings are good, when the page's assets and routes have paths ''' def path_func(name): return test_urls[name] @@ -208,10 +209,28 @@ def test_set_menu_url( mocked_route_path.side_effect = path_func urls_dict = {'pgwui_home': test_urls['pgwui_home']} - expected.update(urls_dict) - urls.set_menu_url(request, urls_dict) + expected_urls.update(urls_dict) + result = urls.set_menu_url(request, urls_dict) - assert urls_dict == expected + assert urls_dict == expected_urls + assert result == [] + + +def test_set_menu_url_bad_page( + pyramid_request_config, mock_method_route_path, mock_url_of_page): + '''The expected urls are returned with errors when the page settings + are bad, when the page's asset or route has no path + ''' + expected_urls = {'home_page': '/', 'pgwui_logout': '/logout'} + + mock_url_of_page.side_effect = common_ex.BadPathError + request = get_current_request + new_urls = expected_urls.copy() + result = urls.set_menu_url(request, new_urls) + + assert new_urls == expected_urls + assert len(result) == 1 + assert isinstance(result[0], common_ex.BadPathError) mock_set_menu_url = testing.make_mock_fixture( @@ -235,20 +254,22 @@ def test_set_component_urls( pyramid_request_config, mock_method_route_path, mock_set_menu_url, mock_find_pgwui_components, test_urls): '''Urls are set for every component which has a route, except for - pgwui_menu + pgwui_menu, expected errors are returned ''' + test_errors = ['some error'] test_components = list(test_urls) + ['pgwui_noroute'] def url_func(url): return test_urls[url] + mock_set_menu_url.return_value = test_errors request = get_current_request() mocked_route_path = mock_method_route_path(request) mocked_route_path.side_effect = url_func mock_find_pgwui_components.return_value = test_components urls_dict = dict() - urls.set_component_urls(request, urls_dict) + result = urls.set_component_urls(request, urls_dict) expected_urls = test_urls.copy() if 'pgwui_menu' in expected_urls: @@ -256,6 +277,7 @@ def test_set_component_urls( mock_set_menu_url.assert_called_once() assert urls_dict == expected_urls + assert result == test_errors mock_set_component_urls = testing.make_mock_fixture( @@ -264,20 +286,44 @@ mock_set_component_urls = testing.make_mock_fixture( # set_urls() -def test_set_urls( +def test_set_urls_good_path( pyramid_request_config, mock_url_of_page, mock_set_component_urls): - '''The 'home' url is added and set_component_urls() called + '''When the 'home_page' route path is good the 'home' url is added, + set_component_urls() called, and the expected errors are returned ''' + component_errors = ['some error'] test_home_route = '/' request = get_current_request() + mock_set_component_urls.return_value = component_errors mock_url_of_page.return_value = test_home_route urls_dict = dict() - urls.set_urls(request, urls_dict) + result = urls.set_urls(request, urls_dict) assert urls_dict['pgwui_home'] == test_home_route mock_set_component_urls.assert_called_once() + assert result == component_errors + + +def test_set_urls_bad_path( + pyramid_request_config, mock_url_of_page, mock_set_component_urls): + '''When the 'home_page' route is bad a bad path error is one of the errors + returned + ''' + component_errors = ['some error'] + home_error = common_ex.BadPathError() + request = get_current_request() + + mock_url_of_page.side_effect = home_error + mock_set_component_urls.return_value = component_errors + + urls_dict = dict() + result = urls.set_urls(request, urls_dict) + + assert 'pgwui_home' in urls_dict + mock_set_component_urls.assert_called_once() + assert home_error in result mock_set_urls = testing.make_mock_fixture( @@ -292,10 +338,12 @@ def test_add_urls_setting( are put in the pgwui dict in the settings ''' caplog.set_level(logging.DEBUG) + expected_errors = ['some error'] expected_urls = {'key1': 'val1', 'key2': 'val2'} def set_urls(request, urls): urls.update(expected_urls) + return expected_errors mock_set_urls.side_effect = set_urls @@ -304,12 +352,14 @@ def test_add_urls_setting( request.registry.settings = request_settings settings = {'pgwui': {}} - urls.add_urls_setting(pyramid_request_config, settings) + result = urls.add_urls_setting(pyramid_request_config, settings) mock_request_blank.blank.assert_called_once() mock_set_urls.assert_called_once() assert settings['pgwui']['urls'] == expected_urls + assert result == expected_errors + logs = caplog.record_tuples assert len(logs) == 1 assert logs[0][1] == logging.DEBUG -- 2.34.1