diff --git a/incorporealcms/__init__.py b/incorporealcms/__init__.py index 726eaa3..6d87421 100644 --- a/incorporealcms/__init__.py +++ b/incorporealcms/__init__.py @@ -3,7 +3,7 @@ import logging import os from logging.config import dictConfig -from flask import Flask, request, send_from_directory +from flask import Flask, request from ._version import get_versions @@ -40,11 +40,6 @@ def create_app(instance_path=None, test_config=None): logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status) return response - @app.route(f'/{app.config["MEDIA_DIR"]}/') - def media_files(filename): - return send_from_directory(os.path.join(app.instance_path, app.config['MEDIA_DIR']), - filename) - from . import error_pages, pages app.register_blueprint(pages.bp) app.register_error_handler(400, error_pages.bad_request) diff --git a/incorporealcms/config.py b/incorporealcms/config.py index bc8c3ba..7e8ee0a 100644 --- a/incorporealcms/config.py +++ b/incorporealcms/config.py @@ -54,3 +54,5 @@ class Config(object): DEFAULT_PAGE_STYLE = 'light' TITLE_SUFFIX = 'incorporeal.org' CONTACT_EMAIL = 'bss@incorporeal.org' + + # specify FAVICON in your instance config.py to override the suou icon diff --git a/incorporealcms/lib.py b/incorporealcms/lib.py index 711c042..5973e1f 100644 --- a/incorporealcms/lib.py +++ b/incorporealcms/lib.py @@ -34,6 +34,7 @@ def render(template_name_or_list, **context): PAGE_STYLES = { 'dark': 'css/dark.css', 'light': 'css/light.css', + 'plain': 'css/plain.css', } selected_style = request.args.get('style', None) diff --git a/incorporealcms/pages.py b/incorporealcms/pages.py index 7e7da0d..7ea9dd2 100644 --- a/incorporealcms/pages.py +++ b/incorporealcms/pages.py @@ -6,7 +6,7 @@ import re from flask import Blueprint, Markup, abort from flask import current_app as app -from flask import redirect, request +from flask import redirect, request, send_from_directory from tzlocal import get_localzone from incorporealcms.lib import get_meta_str, init_md, render @@ -21,8 +21,9 @@ bp = Blueprint('pages', __name__, url_prefix='/') def display_page(path): """Get the file contents of the requested path and render the file.""" try: - resolved_path = request_path_to_instance_resource_path(path) - logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path) + resolved_path, render_type = request_path_to_instance_resource_path(path) + logger.debug("received request for path '%s', resolved to '%s', type '%s'", + path, resolved_path, render_type) except PermissionError: abort(400) except IsADirectoryError: @@ -30,28 +31,39 @@ def display_page(path): except FileNotFoundError: abort(404) - try: - with app.open_instance_resource(resolved_path, 'r') as entry_file: - mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone()) - entry = entry_file.read() - except OSError: - logger.exception("resolved path '%s' could not be opened!", resolved_path) - abort(500) + if render_type == 'file': + return send_from_directory(app.instance_path, resolved_path) + elif render_type == 'symlink': + logger.debug("attempting to redirect path '%s' to reverse of resource '%s'", path, resolved_path) + redirect_path = f'/{instance_resource_path_to_request_path(resolved_path)}' + logger.debug("redirect path: '%s'", redirect_path) + return redirect(redirect_path, code=301) + elif render_type == 'markdown': + try: + with app.open_instance_resource(resolved_path, 'r') as entry_file: + mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone()) + entry = entry_file.read() + except OSError: + logger.exception("resolved path '%s' could not be opened!", resolved_path) + abort(500) + else: + md = init_md() + content = Markup(md.convert(entry)) + logger.debug("file metadata: %s", md.Meta) + + parent_navs = generate_parent_navs(resolved_path) + + page_name = (get_meta_str(md, 'title') if md.Meta.get('title') else + f'/{instance_resource_path_to_request_path(resolved_path)}') + page_title = f'{page_name} - {app.config["TITLE_SUFFIX"]}' if page_name else app.config['TITLE_SUFFIX'] + logger.debug("title (potentially derived): %s", page_title) + + return render('base.html', title=page_title, description=get_meta_str(md, 'description'), + image=get_meta_str(md, 'image'), base_url=request.base_url, content=content, + navs=parent_navs, mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z')) else: - md = init_md() - content = Markup(md.convert(entry)) - logger.debug("file metadata: %s", md.Meta) - - parent_navs = generate_parent_navs(resolved_path) - - page_name = (get_meta_str(md, 'title') if md.Meta.get('title') else - f'/{instance_resource_path_to_request_path(resolved_path)}') - page_title = f'{page_name} - {app.config["TITLE_SUFFIX"]}' if page_name else app.config['TITLE_SUFFIX'] - logger.debug("title (potentially derived): %s", page_title) - - return render('base.html', title=page_title, description=get_meta_str(md, 'description'), - image=get_meta_str(md, 'image'), base_url=request.base_url, content=content, navs=parent_navs, - mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z')) + logger.exception("unsupported render_type '%s'!?", render_type) + abort(500) def request_path_to_instance_resource_path(path): @@ -63,34 +75,52 @@ def request_path_to_instance_resource_path(path): """ # check if the path is allowed base_dir = os.path.realpath(f'{app.instance_path}/pages/') - resolved_path = os.path.realpath(os.path.join(base_dir, path)) - logger.debug("base_dir: %s, constructed resolved_path: %s", base_dir, resolved_path) + verbatim_path = os.path.abspath(os.path.join(base_dir, path)) + resolved_path = os.path.realpath(verbatim_path) + logger.debug("base_dir '%s', constructed resolved_path '%s' for path '%s'", base_dir, resolved_path, path) # bail if the requested real path isn't inside the base directory if base_dir != os.path.commonpath((base_dir, resolved_path)): logger.warning("client tried to request a path '%s' outside of the base_dir!", path) raise PermissionError - # if this is a file-like requset but actually a directory, redirect the user - if os.path.isdir(resolved_path) and not path.endswith('/'): - logger.info("client requested a path '%s' that is actually a directory", path) - raise IsADirectoryError + # see if we have a real file or if we should infer markdown rendering + if os.path.exists(resolved_path): + # if this is a file-like request but actually a directory, redirect the user + if os.path.isdir(resolved_path) and not path.endswith('/'): + logger.info("client requested a path '%s' that is actually a directory", path) + raise IsADirectoryError - # derive the proper markdown file depending on if this is a dir or file - if os.path.isdir(resolved_path): - absolute_resource = os.path.join(resolved_path, 'index.md') - else: - absolute_resource = f'{resolved_path}.md' + # if the requested path contains a symlink, redirect the user + if verbatim_path != resolved_path: + logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path) + return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink' - logger.info("final path = '%s' for request '%s'", absolute_resource, path) + # derive the proper markdown or actual file depending on if this is a dir or file + if os.path.isdir(resolved_path): + resolved_path = os.path.join(resolved_path, 'index.md') + return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown' + + logger.info("final DIRECT path = '%s' for request '%s'", resolved_path, path) + return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'file' + + # if we're here, this isn't direct file access, so try markdown inference + verbatim_path = os.path.abspath(os.path.join(base_dir, f'{path}.md')) + resolved_path = os.path.realpath(verbatim_path) # does the final file actually exist? - if not os.path.exists(absolute_resource): - logger.warning("requested final path '%s' does not exist!", absolute_resource) + if not os.path.exists(resolved_path): + logger.warning("requested final path '%s' does not exist!", resolved_path) raise FileNotFoundError + # check for symlinks + if verbatim_path != resolved_path: + logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path) + return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink' + + logger.info("final path = '%s' for request '%s'", resolved_path, path) # we checked that the file exists via absolute path, but now we need to give the path relative to instance dir - return absolute_resource.replace(f'{app.instance_path}{os.path.sep}', '') + return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown' def instance_resource_path_to_request_path(path): @@ -127,8 +157,19 @@ def generate_parent_navs(path): md = init_md() # read the resource - with app.open_instance_resource(path, 'r') as entry_file: - entry = entry_file.read() - _ = Markup(md.convert(entry)) - page_name = " ".join(md.Meta.get('title')) if md.Meta.get('title') else request_path - return generate_parent_navs(parent_resource_path) + [(page_name, request_path)] + try: + with app.open_instance_resource(path, 'r') as entry_file: + entry = entry_file.read() + _ = Markup(md.convert(entry)) + page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title') + else request_path_to_breadcrumb_display(request_path)) + return generate_parent_navs(parent_resource_path) + [(page_name, request_path)] + except FileNotFoundError: + return generate_parent_navs(parent_resource_path) + [(request_path, request_path)] + + +def request_path_to_breadcrumb_display(path): + """Given a request path, e.g. "/foo/bar/baz/", turn it into breadcrumby text "baz".""" + undired = path.rstrip('/') + leaf = undired[undired.rfind('/'):] + return leaf.strip('/') diff --git a/incorporealcms/static/css/style.css b/incorporealcms/static/css/base.css similarity index 100% rename from incorporealcms/static/css/style.css rename to incorporealcms/static/css/base.css diff --git a/incorporealcms/static/css/dark.css b/incorporealcms/static/css/dark.css index 9c038e0..854d854 100644 --- a/incorporealcms/static/css/dark.css +++ b/incorporealcms/static/css/dark.css @@ -1,3 +1,5 @@ +@import '/static/css/base.css'; + html { color: #CCC; } diff --git a/incorporealcms/static/css/light.css b/incorporealcms/static/css/light.css index 8b268e4..e9b83f3 100644 --- a/incorporealcms/static/css/light.css +++ b/incorporealcms/static/css/light.css @@ -1,3 +1,5 @@ +@import '/static/css/base.css'; + html { color: #222; } diff --git a/incorporealcms/static/css/plain.css b/incorporealcms/static/css/plain.css new file mode 100644 index 0000000..264e44a --- /dev/null +++ b/incorporealcms/static/css/plain.css @@ -0,0 +1,8 @@ + +.img-25 { + max-width: 25% !important; +} + +.img-50 { + max-width: 50% !important; +} diff --git a/incorporealcms/templates/base.html b/incorporealcms/templates/base.html index e15878f..0d71a44 100644 --- a/incorporealcms/templates/base.html +++ b/incorporealcms/templates/base.html @@ -7,9 +7,8 @@ - - +
{% block header %} @@ -23,6 +22,7 @@
{% endblock %} diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index d1a09e2..9cced20 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -16,7 +16,7 @@ click==7.1.2 # via # flask # pip-tools -coverage==5.4 +coverage==5.5 # via pytest-cov distlib==0.3.1 # via virtualenv @@ -30,7 +30,7 @@ flake8-blind-except==0.2.0 # via -r requirements/requirements-dev.in flake8-builtins==1.5.3 # via -r requirements/requirements-dev.in -flake8-docstrings==1.5.0 +flake8-docstrings==1.6.0 # via -r requirements/requirements-dev.in flake8-executable==2.1.1 # via -r requirements/requirements-dev.in @@ -42,7 +42,7 @@ flake8-logging-format==0.6.0 # via -r requirements/requirements-dev.in flake8-mutable==1.2.0 # via -r requirements/requirements-dev.in -flake8==3.8.4 +flake8==3.9.1 # via # -r requirements/requirements-dev.in # dlint @@ -53,19 +53,19 @@ flake8==3.8.4 # flake8-mutable flask==1.1.2 # via -r requirements/requirements.in -gitdb==4.0.5 +gitdb==4.0.7 # via gitpython -gitpython==3.1.13 +gitpython==3.1.14 # via bandit iniconfig==1.1.1 # via pytest -isort==5.7.0 +isort==5.8.0 # via flake8-isort itsdangerous==1.1.0 # via flask jinja2==2.11.3 # via flask -markdown==3.3.3 +markdown==3.3.4 # via # -r requirements/requirements.in # mdx-linkify @@ -82,7 +82,9 @@ packaging==20.9 # tox pbr==5.5.1 # via stevedore -pip-tools==5.5.0 +pep517==0.10.0 + # via pip-tools +pip-tools==6.1.0 # via -r requirements/requirements-dev.in pluggy==0.13.1 # via @@ -92,17 +94,17 @@ py==1.10.0 # via # pytest # tox -pycodestyle==2.6.0 +pycodestyle==2.7.0 # via flake8 -pydocstyle==5.1.1 +pydocstyle==6.0.0 # via flake8-docstrings -pyflakes==2.2.0 +pyflakes==2.3.1 # via flake8 pyparsing==2.4.7 # via packaging pytest-cov==2.11.1 # via -r requirements/requirements-dev.in -pytest==6.2.2 +pytest==6.2.3 # via # -r requirements/requirements-dev.in # pytest-cov @@ -116,7 +118,7 @@ six==1.15.0 # bleach # tox # virtualenv -smmap==3.0.5 +smmap==4.0.0 # via gitdb snowballstemmer==2.1.0 # via pydocstyle @@ -126,11 +128,12 @@ testfixtures==6.17.1 # via flake8-isort toml==0.10.2 # via + # pep517 # pytest # tox tox-wheel==0.6.0 # via -r requirements/requirements-dev.in -tox==3.22.0 +tox==3.23.0 # via # -r requirements/requirements-dev.in # tox-wheel @@ -138,7 +141,7 @@ tzlocal==2.1 # via -r requirements/requirements.in versioneer==0.19 # via -r requirements/requirements-dev.in -virtualenv==20.4.2 +virtualenv==20.4.3 # via tox webencodings==0.5.1 # via bleach diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 94afa16..55d31f8 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -14,7 +14,7 @@ itsdangerous==1.1.0 # via flask jinja2==2.11.3 # via flask -markdown==3.3.3 +markdown==3.3.4 # via # -r requirements/requirements.in # mdx-linkify diff --git a/tests/functional_tests.py b/tests/functional_tests.py index bd3a892..714a5c4 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -1,5 +1,6 @@ """Test page requests.""" import re +from unittest.mock import patch def test_page_that_exists(client): @@ -9,6 +10,13 @@ def test_page_that_exists(client): assert b'

test index

' in response.data +def test_direct_file_that_exists(client): + """Test that the app can serve a basic file at the index.""" + response = client.get('/foo.txt') + assert response.status_code == 200 + assert b'test file' in response.data + + def test_page_that_doesnt_exist(client): """Test that the app returns 404 for nonsense requests and they use my error page.""" response = client.get('/ohuesthaoeusth') @@ -34,6 +42,24 @@ def test_internal_server_error_serves_error_page(client): assert b'bss@incorporeal.org' in response.data +def test_oserror_is_500(client, app): + """Test that an OSError raises as a 500.""" + with app.test_request_context(): + with patch('flask.current_app.open_instance_resource', side_effect=OSError): + response = client.get('/') + assert response.status_code == 500 + assert b'INTERNAL SERVER ERROR' in response.data + + +def test_unsupported_file_type_is_500(client, app): + """Test a coding condition mishap raises as a 500.""" + with app.test_request_context(): + with patch('incorporealcms.pages.request_path_to_instance_resource_path', return_value=('foo', 'bar')): + response = client.get('/') + assert response.status_code == 500 + assert b'INTERNAL SERVER ERROR' in response.data + + def test_weird_paths_do_not_get_served(client): """Test that we clean up requests as desired.""" response = client.get('/../../') @@ -101,6 +127,44 @@ def test_that_page_request_redirects_to_directory(client): """ response = client.get('/subdir') assert response.status_code == 301 + assert response.location == 'http://localhost/subdir/' + + +def test_that_request_to_symlink_redirects_markdown(client): + """Test that a request to /foo redirects to /what-foo-points-at.""" + response = client.get('/symlink-to-no-title') + assert response.status_code == 301 + assert response.location == 'http://localhost/no-title' + + +def test_that_request_to_symlink_redirects_file(client): + """Test that a request to /foo.txt redirects to /what-foo-points-at.txt.""" + response = client.get('/symlink-to-foo.txt') + assert response.status_code == 301 + assert response.location == 'http://localhost/foo.txt' + + +def test_that_request_to_symlink_redirects_directory(client): + """Test that a request to /foo/ redirects to /what-foo-points-at/.""" + response = client.get('/symlink-to-subdir/') + assert response.status_code == 301 + assert response.location == 'http://localhost/subdir' + # sadly, this location also redirects + response = client.get('/subdir') + assert response.status_code == 301 + assert response.location == 'http://localhost/subdir/' + # but we do get there + response = client.get('/subdir/') + assert response.status_code == 200 + + +def test_that_request_to_symlink_redirects_subdirectory(client): + """Test that a request to /foo/bar redirects to /what-foo-points-at/bar.""" + response = client.get('/symlink-to-subdir/page-no-title') + assert response.status_code == 301 + assert response.location == 'http://localhost/subdir/page-no-title' + response = client.get('/subdir/page-no-title') + assert response.status_code == 200 def test_that_dir_request_does_not_redirect(client): diff --git a/tests/instance/pages/bss-square-no-bg.png b/tests/instance/pages/bss-square-no-bg.png new file mode 100644 index 0000000..c592d25 Binary files /dev/null and b/tests/instance/pages/bss-square-no-bg.png differ diff --git a/tests/instance/pages/foo.txt b/tests/instance/pages/foo.txt new file mode 100644 index 0000000..16b14f5 --- /dev/null +++ b/tests/instance/pages/foo.txt @@ -0,0 +1 @@ +test file diff --git a/tests/instance/media/favicon.png b/tests/instance/pages/media/favicon.png similarity index 100% rename from tests/instance/media/favicon.png rename to tests/instance/pages/media/favicon.png diff --git a/tests/instance/pages/no-index-dir/page.md b/tests/instance/pages/no-index-dir/page.md new file mode 100644 index 0000000..2bcf858 --- /dev/null +++ b/tests/instance/pages/no-index-dir/page.md @@ -0,0 +1 @@ +this is a test page diff --git a/tests/instance/pages/symlink-to-foo.txt b/tests/instance/pages/symlink-to-foo.txt new file mode 120000 index 0000000..996f178 --- /dev/null +++ b/tests/instance/pages/symlink-to-foo.txt @@ -0,0 +1 @@ +foo.txt \ No newline at end of file diff --git a/tests/instance/pages/symlink-to-no-title.md b/tests/instance/pages/symlink-to-no-title.md new file mode 120000 index 0000000..e4c6749 --- /dev/null +++ b/tests/instance/pages/symlink-to-no-title.md @@ -0,0 +1 @@ +no-title.md \ No newline at end of file diff --git a/tests/instance/pages/symlink-to-subdir b/tests/instance/pages/symlink-to-subdir new file mode 120000 index 0000000..8bbe8a5 --- /dev/null +++ b/tests/instance/pages/symlink-to-subdir @@ -0,0 +1 @@ +subdir \ No newline at end of file diff --git a/tests/test_factory.py b/tests/test_factory.py index b0a2a3f..1a99425 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -58,3 +58,13 @@ def test_media_file_access(client): response = client.get('/media/favicon.png') assert response.status_code == 200 assert response.headers['content-type'] == 'image/png' + + +def test_favicon_override(): + """Test that a configuration with a specific favicon overrides the default.""" + instance_path = os.path.join(HERE, 'instance') + app = create_app(instance_path=instance_path, test_config={'FAVICON': '/media/foo.png'}) + client = app.test_client() + response = client.get('/no-title') + assert response.status_code == 200 + assert b'' in response.data diff --git a/tests/test_pages.py b/tests/test_pages.py index ce41165..839ffe3 100644 --- a/tests/test_pages.py +++ b/tests/test_pages.py @@ -3,7 +3,7 @@ import pytest from werkzeug.http import dump_cookie from incorporealcms.pages import (generate_parent_navs, instance_resource_path_to_request_path, render, - request_path_to_instance_resource_path) + request_path_to_breadcrumb_display, request_path_to_instance_resource_path) def test_generate_page_navs_index(app): @@ -15,13 +15,13 @@ def test_generate_page_navs_index(app): def test_generate_page_navs_subdir_index(app): """Test that dir pages have navs to the root and themselves.""" with app.app_context(): - assert generate_parent_navs('pages/subdir/index.md') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/')] + assert generate_parent_navs('pages/subdir/index.md') == [('incorporeal.org', '/'), ('subdir', '/subdir/')] def test_generate_page_navs_subdir_real_page(app): """Test that real pages have navs to the root, their parent, and themselves.""" with app.app_context(): - assert generate_parent_navs('pages/subdir/page.md') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/'), + assert generate_parent_navs('pages/subdir/page.md') == [('incorporeal.org', '/'), ('subdir', '/subdir/'), ('Page', '/subdir/page')] @@ -31,7 +31,17 @@ def test_generate_page_navs_subdir_with_title_parsing_real_page(app): assert generate_parent_navs('pages/subdir-with-title/page.md') == [ ('incorporeal.org', '/'), ('SUB!', '/subdir-with-title/'), - ('/subdir-with-title/page', '/subdir-with-title/page') + ('page', '/subdir-with-title/page') + ] + + +def test_generate_page_navs_subdir_with_no_index(app): + """Test that breadcrumbs still generate even if a subdir doesn't have an index.md.""" + with app.app_context(): + assert generate_parent_navs('pages/no-index-dir/page.md') == [ + ('incorporeal.org', '/'), + ('/no-index-dir/', '/no-index-dir/'), + ('page', '/no-index-dir/page') ] @@ -60,44 +70,45 @@ def test_render_with_no_user_theme(app): def test_request_path_to_instance_resource_path(app): """Test a normal URL request is transformed into the file path.""" with app.test_request_context(): - assert request_path_to_instance_resource_path('index') == 'pages/index.md' + assert request_path_to_instance_resource_path('index') == ('pages/index.md', 'markdown') def test_request_path_to_instance_resource_path_direct_file(app): """Test a normal URL request is transformed into the file path.""" with app.test_request_context(): - assert request_path_to_instance_resource_path('no-title') == 'pages/no-title.md' + assert request_path_to_instance_resource_path('no-title') == ('pages/no-title.md', 'markdown') def test_request_path_to_instance_resource_path_in_subdir(app): """Test a normal URL request is transformed into the file path.""" with app.test_request_context(): - assert request_path_to_instance_resource_path('subdir/page') == 'pages/subdir/page.md' + assert request_path_to_instance_resource_path('subdir/page') == ('pages/subdir/page.md', 'markdown') def test_request_path_to_instance_resource_path_subdir_index(app): """Test a normal URL request is transformed into the file path.""" with app.test_request_context(): - assert request_path_to_instance_resource_path('subdir/') == 'pages/subdir/index.md' + assert request_path_to_instance_resource_path('subdir/') == ('pages/subdir/index.md', 'markdown') def test_request_path_to_instance_resource_path_relatives_walked(app): """Test a normal URL request is transformed into the file path.""" with app.test_request_context(): assert (request_path_to_instance_resource_path('subdir/more-subdir/../../more-metadata') == - 'pages/more-metadata.md') + ('pages/more-metadata.md', 'markdown')) def test_request_path_to_instance_resource_path_relatives_walked_indexes_work_too(app): """Test a normal URL request is transformed into the file path.""" with app.test_request_context(): - assert request_path_to_instance_resource_path('subdir/more-subdir/../../') == 'pages/index.md' + assert request_path_to_instance_resource_path('subdir/more-subdir/../../') == ('pages/index.md', 'markdown') def test_request_path_to_instance_resource_path_relatives_walked_into_subdirs_also_fine(app): """Test a normal URL request is transformed into the file path.""" with app.test_request_context(): - assert request_path_to_instance_resource_path('subdir/more-subdir/../../subdir/page') == 'pages/subdir/page.md' + assert (request_path_to_instance_resource_path('subdir/more-subdir/../../subdir/page') == + ('pages/subdir/page.md', 'markdown')) def test_request_path_to_instance_resource_path_permission_error_on_ref_above_pages(app): @@ -114,6 +125,41 @@ def test_request_path_to_instance_resource_path_isadirectory_on_file_like_req_fo assert request_path_to_instance_resource_path('subdir') +def test_request_path_to_instance_resource_path_actual_file(app): + """Test that a request for e.g. '/foo.png' when foo.png is a real file works.""" + with app.test_request_context(): + assert (request_path_to_instance_resource_path('bss-square-no-bg.png') == + ('pages/bss-square-no-bg.png', 'file')) + + +def test_request_path_to_instance_resource_path_markdown_symlink(app): + """Test that a request for e.g. '/foo' when foo.md is a symlink to another .md file redirects.""" + with app.test_request_context(): + assert (request_path_to_instance_resource_path('symlink-to-no-title') == + ('pages/no-title.md', 'symlink')) + + +def test_request_path_to_instance_resource_path_file_symlink(app): + """Test that a request for e.g. '/foo' when foo.txt is a symlink to another .txt file redirects.""" + with app.test_request_context(): + assert (request_path_to_instance_resource_path('symlink-to-foo.txt') == + ('pages/foo.txt', 'symlink')) + + +def test_request_path_to_instance_resource_path_dir_symlink(app): + """Test that a request for e.g. '/foo' when /foo is a symlink to /bar redirects.""" + with app.test_request_context(): + assert (request_path_to_instance_resource_path('symlink-to-subdir/') == + ('pages/subdir', 'symlink')) + + +def test_request_path_to_instance_resource_path_subdir_symlink(app): + """Test that a request for e.g. '/foo/baz' when /foo is a symlink to /bar redirects.""" + with app.test_request_context(): + assert (request_path_to_instance_resource_path('symlink-to-subdir/page-no-title') == + ('pages/subdir/page-no-title.md', 'symlink')) + + def test_request_path_to_instance_resource_path_nonexistant_file_errors(app): """Test that a request for something not on disk errors.""" with app.test_request_context(): @@ -155,10 +201,21 @@ def test_instance_resource_path_to_request_path_on_subdir_and_page(app): def test_request_resource_request_root(app): """Test that a request can resolve to a resource and back to a request.""" with app.test_request_context(): - instance_resource_path_to_request_path(request_path_to_instance_resource_path('index')) == '' + instance_path, _ = request_path_to_instance_resource_path('index') + instance_resource_path_to_request_path(instance_path) == '' def test_request_resource_request_page(app): """Test that a request can resolve to a resource and back to a request.""" with app.test_request_context(): - instance_resource_path_to_request_path(request_path_to_instance_resource_path('no-title')) == 'no-title' + instance_path, _ = request_path_to_instance_resource_path('no-title') + instance_resource_path_to_request_path(instance_path) == 'no-title' + + +def test_request_path_to_breadcrumb_display_patterns(): + """Test various conversions from request path to leaf nodes for display in the breadcrumbs.""" + assert request_path_to_breadcrumb_display('/foo') == 'foo' + assert request_path_to_breadcrumb_display('/foo/') == 'foo' + assert request_path_to_breadcrumb_display('/foo/bar') == 'bar' + assert request_path_to_breadcrumb_display('/foo/bar/') == 'bar' + assert request_path_to_breadcrumb_display('/') == ''