Merge branch 'master' of git.incorporeal.org:bss/incorporeal-cms

This commit is contained in:
Brian S. Stephan 2021-04-17 14:57:20 -05:00
commit da447d2873
21 changed files with 271 additions and 81 deletions

View File

@ -3,7 +3,7 @@ import logging
import os import os
from logging.config import dictConfig from logging.config import dictConfig
from flask import Flask, request, send_from_directory from flask import Flask, request
from ._version import get_versions 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) logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response return response
@app.route(f'/{app.config["MEDIA_DIR"]}/<path:filename>')
def media_files(filename):
return send_from_directory(os.path.join(app.instance_path, app.config['MEDIA_DIR']),
filename)
from . import error_pages, pages from . import error_pages, pages
app.register_blueprint(pages.bp) app.register_blueprint(pages.bp)
app.register_error_handler(400, error_pages.bad_request) app.register_error_handler(400, error_pages.bad_request)

View File

@ -54,3 +54,5 @@ class Config(object):
DEFAULT_PAGE_STYLE = 'light' DEFAULT_PAGE_STYLE = 'light'
TITLE_SUFFIX = 'incorporeal.org' TITLE_SUFFIX = 'incorporeal.org'
CONTACT_EMAIL = 'bss@incorporeal.org' CONTACT_EMAIL = 'bss@incorporeal.org'
# specify FAVICON in your instance config.py to override the suou icon

View File

@ -34,6 +34,7 @@ def render(template_name_or_list, **context):
PAGE_STYLES = { PAGE_STYLES = {
'dark': 'css/dark.css', 'dark': 'css/dark.css',
'light': 'css/light.css', 'light': 'css/light.css',
'plain': 'css/plain.css',
} }
selected_style = request.args.get('style', None) selected_style = request.args.get('style', None)

View File

@ -6,7 +6,7 @@ import re
from flask import Blueprint, Markup, abort from flask import Blueprint, Markup, abort
from flask import current_app as app 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 tzlocal import get_localzone
from incorporealcms.lib import get_meta_str, init_md, render from incorporealcms.lib import get_meta_str, init_md, render
@ -21,8 +21,9 @@ bp = Blueprint('pages', __name__, url_prefix='/')
def display_page(path): def display_page(path):
"""Get the file contents of the requested path and render the file.""" """Get the file contents of the requested path and render the file."""
try: try:
resolved_path = request_path_to_instance_resource_path(path) resolved_path, render_type = request_path_to_instance_resource_path(path)
logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path) logger.debug("received request for path '%s', resolved to '%s', type '%s'",
path, resolved_path, render_type)
except PermissionError: except PermissionError:
abort(400) abort(400)
except IsADirectoryError: except IsADirectoryError:
@ -30,28 +31,39 @@ def display_page(path):
except FileNotFoundError: except FileNotFoundError:
abort(404) abort(404)
try: if render_type == 'file':
with app.open_instance_resource(resolved_path, 'r') as entry_file: return send_from_directory(app.instance_path, resolved_path)
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone()) elif render_type == 'symlink':
entry = entry_file.read() logger.debug("attempting to redirect path '%s' to reverse of resource '%s'", path, resolved_path)
except OSError: redirect_path = f'/{instance_resource_path_to_request_path(resolved_path)}'
logger.exception("resolved path '%s' could not be opened!", resolved_path) logger.debug("redirect path: '%s'", redirect_path)
abort(500) 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: else:
md = init_md() logger.exception("unsupported render_type '%s'!?", render_type)
content = Markup(md.convert(entry)) abort(500)
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'))
def request_path_to_instance_resource_path(path): 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 # check if the path is allowed
base_dir = os.path.realpath(f'{app.instance_path}/pages/') base_dir = os.path.realpath(f'{app.instance_path}/pages/')
resolved_path = os.path.realpath(os.path.join(base_dir, path)) verbatim_path = os.path.abspath(os.path.join(base_dir, path))
logger.debug("base_dir: %s, constructed resolved_path: %s", base_dir, resolved_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 # bail if the requested real path isn't inside the base directory
if base_dir != os.path.commonpath((base_dir, resolved_path)): 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) logger.warning("client tried to request a path '%s' outside of the base_dir!", path)
raise PermissionError raise PermissionError
# if this is a file-like requset but actually a directory, redirect the user # see if we have a real file or if we should infer markdown rendering
if os.path.isdir(resolved_path) and not path.endswith('/'): if os.path.exists(resolved_path):
logger.info("client requested a path '%s' that is actually a directory", path) # if this is a file-like request but actually a directory, redirect the user
raise IsADirectoryError 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 the requested path contains a symlink, redirect the user
if os.path.isdir(resolved_path): if verbatim_path != resolved_path:
absolute_resource = os.path.join(resolved_path, 'index.md') logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path)
else: return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink'
absolute_resource = f'{resolved_path}.md'
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? # does the final file actually exist?
if not os.path.exists(absolute_resource): if not os.path.exists(resolved_path):
logger.warning("requested final path '%s' does not exist!", absolute_resource) logger.warning("requested final path '%s' does not exist!", resolved_path)
raise FileNotFoundError 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 # 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): def instance_resource_path_to_request_path(path):
@ -127,8 +157,19 @@ def generate_parent_navs(path):
md = init_md() md = init_md()
# read the resource # read the resource
with app.open_instance_resource(path, 'r') as entry_file: try:
entry = entry_file.read() with app.open_instance_resource(path, 'r') as entry_file:
_ = Markup(md.convert(entry)) entry = entry_file.read()
page_name = " ".join(md.Meta.get('title')) if md.Meta.get('title') else request_path _ = Markup(md.convert(entry))
return generate_parent_navs(parent_resource_path) + [(page_name, request_path)] 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('/')

View File

@ -1,3 +1,5 @@
@import '/static/css/base.css';
html { html {
color: #CCC; color: #CCC;
} }

View File

@ -1,3 +1,5 @@
@import '/static/css/base.css';
html { html {
color: #222; color: #222;
} }

View File

@ -0,0 +1,8 @@
.img-25 {
max-width: 25% !important;
}
.img-50 {
max-width: 50% !important;
}

View File

@ -7,9 +7,8 @@
<meta property="og:url" content="{{ base_url }}"> <meta property="og:url" content="{{ base_url }}">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}"> <link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}"> <link rel="icon" href="{% if config.FAVICON %}{{ config.FAVICON }}{% else %}{{ url_for('static', filename='img/favicon.png') }}{% endif %}">
<div class="site-wrap"> <div class="site-wrap">
{% block header %} {% block header %}
@ -23,6 +22,7 @@
<div class="styles"> <div class="styles">
<a href="?style=dark">[dark]</a> <a href="?style=dark">[dark]</a>
<a href="?style=light">[light]</a> <a href="?style=light">[light]</a>
<a href="?style=plain">[plain]</a>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -16,7 +16,7 @@ click==7.1.2
# via # via
# flask # flask
# pip-tools # pip-tools
coverage==5.4 coverage==5.5
# via pytest-cov # via pytest-cov
distlib==0.3.1 distlib==0.3.1
# via virtualenv # via virtualenv
@ -30,7 +30,7 @@ flake8-blind-except==0.2.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-builtins==1.5.3 flake8-builtins==1.5.3
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-docstrings==1.5.0 flake8-docstrings==1.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-executable==2.1.1 flake8-executable==2.1.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
@ -42,7 +42,7 @@ flake8-logging-format==0.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-mutable==1.2.0 flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8==3.8.4 flake8==3.9.1
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# dlint # dlint
@ -53,19 +53,19 @@ flake8==3.8.4
# flake8-mutable # flake8-mutable
flask==1.1.2 flask==1.1.2
# via -r requirements/requirements.in # via -r requirements/requirements.in
gitdb==4.0.5 gitdb==4.0.7
# via gitpython # via gitpython
gitpython==3.1.13 gitpython==3.1.14
# via bandit # via bandit
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
isort==5.7.0 isort==5.8.0
# via flake8-isort # via flake8-isort
itsdangerous==1.1.0 itsdangerous==1.1.0
# via flask # via flask
jinja2==2.11.3 jinja2==2.11.3
# via flask # via flask
markdown==3.3.3 markdown==3.3.4
# via # via
# -r requirements/requirements.in # -r requirements/requirements.in
# mdx-linkify # mdx-linkify
@ -82,7 +82,9 @@ packaging==20.9
# tox # tox
pbr==5.5.1 pbr==5.5.1
# via stevedore # 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 # via -r requirements/requirements-dev.in
pluggy==0.13.1 pluggy==0.13.1
# via # via
@ -92,17 +94,17 @@ py==1.10.0
# via # via
# pytest # pytest
# tox # tox
pycodestyle==2.6.0 pycodestyle==2.7.0
# via flake8 # via flake8
pydocstyle==5.1.1 pydocstyle==6.0.0
# via flake8-docstrings # via flake8-docstrings
pyflakes==2.2.0 pyflakes==2.3.1
# via flake8 # via flake8
pyparsing==2.4.7 pyparsing==2.4.7
# via packaging # via packaging
pytest-cov==2.11.1 pytest-cov==2.11.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
pytest==6.2.2 pytest==6.2.3
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# pytest-cov # pytest-cov
@ -116,7 +118,7 @@ six==1.15.0
# bleach # bleach
# tox # tox
# virtualenv # virtualenv
smmap==3.0.5 smmap==4.0.0
# via gitdb # via gitdb
snowballstemmer==2.1.0 snowballstemmer==2.1.0
# via pydocstyle # via pydocstyle
@ -126,11 +128,12 @@ testfixtures==6.17.1
# via flake8-isort # via flake8-isort
toml==0.10.2 toml==0.10.2
# via # via
# pep517
# pytest # pytest
# tox # tox
tox-wheel==0.6.0 tox-wheel==0.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
tox==3.22.0 tox==3.23.0
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# tox-wheel # tox-wheel
@ -138,7 +141,7 @@ tzlocal==2.1
# via -r requirements/requirements.in # via -r requirements/requirements.in
versioneer==0.19 versioneer==0.19
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
virtualenv==20.4.2 virtualenv==20.4.3
# via tox # via tox
webencodings==0.5.1 webencodings==0.5.1
# via bleach # via bleach

View File

@ -14,7 +14,7 @@ itsdangerous==1.1.0
# via flask # via flask
jinja2==2.11.3 jinja2==2.11.3
# via flask # via flask
markdown==3.3.3 markdown==3.3.4
# via # via
# -r requirements/requirements.in # -r requirements/requirements.in
# mdx-linkify # mdx-linkify

View File

@ -1,5 +1,6 @@
"""Test page requests.""" """Test page requests."""
import re import re
from unittest.mock import patch
def test_page_that_exists(client): def test_page_that_exists(client):
@ -9,6 +10,13 @@ def test_page_that_exists(client):
assert b'<h1>test index</h1>' in response.data assert b'<h1>test index</h1>' 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): def test_page_that_doesnt_exist(client):
"""Test that the app returns 404 for nonsense requests and they use my error page.""" """Test that the app returns 404 for nonsense requests and they use my error page."""
response = client.get('/ohuesthaoeusth') 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 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): def test_weird_paths_do_not_get_served(client):
"""Test that we clean up requests as desired.""" """Test that we clean up requests as desired."""
response = client.get('/../../') response = client.get('/../../')
@ -101,6 +127,44 @@ def test_that_page_request_redirects_to_directory(client):
""" """
response = client.get('/subdir') response = client.get('/subdir')
assert response.status_code == 301 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): def test_that_dir_request_does_not_redirect(client):

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1 @@
test file

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1 @@
this is a test page

View File

@ -0,0 +1 @@
foo.txt

View File

@ -0,0 +1 @@
no-title.md

View File

@ -0,0 +1 @@
subdir

View File

@ -58,3 +58,13 @@ def test_media_file_access(client):
response = client.get('/media/favicon.png') response = client.get('/media/favicon.png')
assert response.status_code == 200 assert response.status_code == 200
assert response.headers['content-type'] == 'image/png' 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'<link rel="icon" href="/media/foo.png">' in response.data

View File

@ -3,7 +3,7 @@ import pytest
from werkzeug.http import dump_cookie from werkzeug.http import dump_cookie
from incorporealcms.pages import (generate_parent_navs, instance_resource_path_to_request_path, render, 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): 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): def test_generate_page_navs_subdir_index(app):
"""Test that dir pages have navs to the root and themselves.""" """Test that dir pages have navs to the root and themselves."""
with app.app_context(): 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): def test_generate_page_navs_subdir_real_page(app):
"""Test that real pages have navs to the root, their parent, and themselves.""" """Test that real pages have navs to the root, their parent, and themselves."""
with app.app_context(): 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')] ('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') == [ assert generate_parent_navs('pages/subdir-with-title/page.md') == [
('incorporeal.org', '/'), ('incorporeal.org', '/'),
('SUB!', '/subdir-with-title/'), ('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): def test_request_path_to_instance_resource_path(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): 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): def test_request_path_to_instance_resource_path_direct_file(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): 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): def test_request_path_to_instance_resource_path_in_subdir(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): 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): def test_request_path_to_instance_resource_path_subdir_index(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): 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): def test_request_path_to_instance_resource_path_relatives_walked(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert (request_path_to_instance_resource_path('subdir/more-subdir/../../more-metadata') == 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): 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.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): 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): 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.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): 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): 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') 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): def test_request_path_to_instance_resource_path_nonexistant_file_errors(app):
"""Test that a request for something not on disk errors.""" """Test that a request for something not on disk errors."""
with app.test_request_context(): 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): def test_request_resource_request_root(app):
"""Test that a request can resolve to a resource and back to a request.""" """Test that a request can resolve to a resource and back to a request."""
with app.test_request_context(): 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): def test_request_resource_request_page(app):
"""Test that a request can resolve to a resource and back to a request.""" """Test that a request can resolve to a resource and back to a request."""
with app.test_request_context(): 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('/') == ''